Connecting...

W1siziisimnvbxbpbgvkx3rozw1lx2fzc2v0cy9zawduawz5lxrly2hub2xvz3kvanbnl2jhbm5lci1kzwzhdwx0lmpwzyjdxq

Exploring Property-Based Testing with ScalaCheck (simple examples) by Nicolas A Perez

W1siziisijiwmtkvmdivmjevmtuvmjkvmzgvnjg5l3blegvscy1wag90by00ntk3otmuanblzyjdlfsiccisinrodw1iiiwiotawedkwmfx1mdazzsjdxq

Let's explore some property-based testing with ScalaCheck!

Software Engineer, Nicolas A Perez gives us some great examples of different problems and solutions to them so you can constantly improve your code.

 

 

This is more examples than talk.

Add ScalaCheck to your build.sbt file

1 libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.13.4" % "test"

First Problem, FizzBuzz

Let’s look at some tests for FizzBuzz problem.

1 object FizzBuzzSpecifications extends Properties("FizzBuzz") {
2   
3   val divBy3 = Gen.choose(1, 1000000) map(_ * 3) filter(_ % 5 != 0)
4   val divBy5 = Gen.choose(1, 1000000) map(_ * 5) filter(_ % 3 != 0)
5   val divBy15 = Gen.choose(1, 1000000) map(_ * 15)
6 
7   property("only div by 3") = forAll (divBy3){ n: Int =>
8     FizzBuzz.translate(n) == "Fizz"
9   }
10 
11   property("only div by 5") = forAll(divBy5) { n: Int =>
12     FizzBuzz.translate(n) == "Buzz"
13   }
14 
15   property("div by 3 & 5") = forAll(divBy15) { n: Int =>
16     FizzBuzz.translate(n) == "FizzBuzz"
17   }
18 
19   property("regular ones") = forAll { n: Int =>
20     (n % 3 != 0 && n % 5 != 0) ==> (FizzBuzz.translate(n) == n.toString)
21   }
22 }

Each test is trying to prove certain property of FizzBuzz, for instance, property “only div by 3” is trying to prove that Fizz is only returned when the passed value is divisible by 3 and not by 5. Taking a look at the value generator (divBy3) will allow us to realized we are filtering out those that are divisible by 5.

Following the same like of thinking we can prove other properties out such as only those that are divisible by 5 and not by 3 should be translated to “Buzz”, or if the value is not divisible by 3 and by 5 it should be translated using the id function and toString, so they become themselves in string format.

The magic here is that each test will run at least 100 times with a randomized value space (that is not stable) based on our generator properties. This is, in my opinion, stronger that testing for using limited values of the problem domain, such as 5, 10, 15, 2, 24, 2.

On the other hand, this testing techniques should not replace TDD but rather extend it for more complete test suite.

Our implementation of FizzBuzz looks like this.

1 object FizzBuzz {
2   def translate(input: Int): String = (input % 3, input % 5) match {
3     case (0, 0) =>  "FizzBuzz"
4     case (0, _) =>  "Fizz"
5     case (_, 0) =>  "Buzz"
6     case (_, _) =>  input.toString
7   }
8 }

 

Second Problem, Stack

Now, let’s see how we can use the same technique to test a custom, yet functional stack implementation.

 

Size

Let’s start by the tests since they are our area of focus in the post.

1 object StackSpecs extends Properties("Stack") {
2
3   val positives = Gen.chooseNum(0, 1000)
4
5   property("push") = forAll(positives) { pushes: Int =>
6     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
7 
8     stack.size == pushes
9   }
10 }

First, we define a value generator, specifically for positives int values.

Then we define our first test that push a number of values (in order) to the stack and verify that the size of the stack corresponds to the number of values that were pushed.

Remember, this same test will run 100 times with a different number of values to be pushed. The first time it runs might be pushing 2 values and verifying that the size is 2 and then it might run again and pushes 97 values and again verifies that the size of the stack is 97.

 

Pick

Our second test verifies that pick from the stack does not modify it.

1 object StackSpecs extends Properties("Stack") {
2
3   val positives = Gen.chooseNum(0, 1000)
4
5   property("push") = forAll(positives) { pushes: Int =>
6     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
7 
8     stack.size == pushes
9   }
10 
11   property("pick") = forAll(positives) { pushes: Int =>
12     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
13 
14     val other = (1 to pushes).foldLeft(stack)((s, _) => {s.head; s})
15 
16     other.size == pushes
17   }
18 }
Using the same principle, the test runs a lot of times, pushing a different number of items into the stack every time and then calling '.head' (pick) and then verifying that the size is equals to the number of values that were pushed, which means '.head' does not modify the stack.
 

toList

Now, we can test getting a list representation of the stack (does not modify the stack).

1 object StackSpecs extends Properties("Stack") {
2 
3   val positives = Gen.chooseNum(0, 1000)
4 
5   property("push") = forAll(positives) { pushes: Int =>
6     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
7 
8     stack.size == pushes
9   }
10 
11   property("pick") = forAll(positives) { pushes: Int =>
12     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
13 
14     val other = (1 to pushes).foldLeft(stack)((s, _) => {s.head; s})
15 
16     other.size == pushes
17   }
18 
19   property("toList") = forAll(positives) { pushes: Int =>
20     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
21 
22     val result = stack.toList()
23     val other = (1 to pushes).reverse.toList
24 
25     result == other
26   }
27 }

Once more, using the same principle, each time the test runs, it checks that '.toList' returns the same values that we pushed in reversed order.

 

Push & Pull

1 object StackSpecs extends Properties("Stack") {
2 
3   val positives = Gen.chooseNum(0, 1000)
4 
5   property("push") = forAll(positives) { pushes: Int =>
6     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
7 
8     stack.size == pushes
9   }
10 
11   property("pick") = forAll(positives) { pushes: Int =>
12     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
13 
14     val other = (1 to pushes).foldLeft(stack)((s, _) => {s.head; s})
15 
16     other.size == pushes
17   }
18 
19   property("toList") = forAll(positives) { pushes: Int =>
20     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
21 
22     val result = stack.toList()
23     val other = (1 to pushes).reverse.toList
24 
25     result == other
26   }
27   
28   property("push pull") = forAll(positives) { pushes: Int =>
29     val stack = (1 to pushes).foldLeft(NStack.empty[Int])((s, value) => s.push(value))
30 
31     val (result, s) = drainWithPull(stack)
32 
33     s == NStack.empty
34 
35     result == stack.toList()
36 
37   }
38 
39   def drainWithPull[A](stack: NStack[A]) = {
40 
41     def drain(s: NStack[A], xs: List[A]): (List[A], NStack[A]) = s match {
42       case EmptyStack           =>  (xs, s)
43       case NItem(a, None)       =>  (a :: xs, NStack.empty)
44       case NItem(a, Some(tail)) =>  drain(tail, a :: xs)
45     }
46 
47     val (xs, s) = drain(stack, List.empty)
48     (xs.reverse, s)
49   }
50 }

Our “push pull” test verifies that whatever we pushed into the stack can be pulled out in the right order. Again, this test will run 100 times with different stack sizes.

 

Stack

Our Stack is an inmutable data structure. Operations on the stack does not modify it but rather create new stacks.

1 sealed trait NStack[+A] {
2 
3   def push[U >: A](value: U): NStack[U]
4 
5   def pull(): (Option[A], NStack[A])
6 
7   def size(): Int
8 
9   def toList(): List[A] = this match {
10     case EmptyStack           =>  List.empty
11     case NItem(v, None)       =>  List(v)
12     case NItem(v, Some(tail)) =>  v :: tail.toList()
13   }
14 
15   def head(): Option[A] = this match {
16     case EmptyStack   =>  None
17     case NItem(v, _)  =>  Some(v)
18   }
19 }
20 
21 object NStack {
22   def empty[A]: NStack[A] = EmptyStack.asInstanceOf[NStack[A]]
23 
24   def apply[A](value: A): NStack[A] = NItem(value)
25 
26   case object EmptyStack extends NStack[Nothing] {
27     override def size(): Int = 0
28 
29     override def push[U >: Nothing](value: U): NStack[U] = NStack(value)
30 
31     override def pull(): (Option[Nothing], NStack[Nothing]) = (None, this)
32   }
33 
34   case class NItem[+A](a: A, tail: Option[NItem[A]] = None) extends NStack[A] {
35 
36     override def size(): Int = tail match {
37       case None       =>  1
38       case Some(item) =>  1 + item.size()
39     }
40 
41     override def push[U >: A](value: U): NStack[U] = NItem(value, Some(this))
42 
43     override def pull(): (Option[A], NStack[A]) = (Some(a), tail.get)
44   }
45 }

 

Conclusions

Property-based testing is a powerful tool to have in your side. I have found it very useful when writing complex data structure like the ones we have been adding to Dogs. However, it should not replace other techniques such as TDD (Test Driven Development) but rather complement them.​'
 
 
This article was written by Nicolas A Perez and posted originally on hackernoon.com