Property Based Testing & FP Abstractions

The Purpose of this workshop is to :

  • Learn new testing approach - Property Based Testing.
  • Learn practical PBT tool - ScalaCheck.
  • Observe some interesting Functional Programming mechanisms and abstractions.
  • Gain more motivation to learn FP :)

This approach should expose people to some abstract functional contructs without need to understand every details of their implementation (because we are going to write only tests).

Exercises

Because participants may have different experience so beside mandatory exercises there are also some additional exercises so when someone will finish mandatory part may start additional part without logging to facebook :)

Examples Can be found here : https://github.com/PawelWlodarski/workshops/tree/master/src/main/scala/jug/lodz/workshops/propertybased

Exercises and Answers can be found here : https://github.com/PawelWlodarski/workshops/tree/master/src/test/scala/jug/lodz/workshops/propertybased

Start

Imagine you want to test the following functionality :

 object MathLib{
    //Sum of the first n natural numbers -> SUM=n*(n+1)/2
    def sumN(n:Int)=n*(n+1)/2
  }

With Unit testing you would write one or couple happy tests and maybe some additional tests for edge cases like 0 or -5. Yet this is far from checking if given rule or property is working properly for all cases.

And this is exactly what we are going to write!

Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/propertybased/exercises/HelloPBTSpec.scala

And take a look at the following test :

property("for every n>0 where n is Natural SUM(1,2...n)=n*(n+1)/2"){
      forAll{(n:Int)=>
        println(n) // just for presentation
        MathLib.sumN(n) shouldBe ((1 to n).sum)
      }
  }

After you run it you may see output similar to the following one:

-482947512
0
-241473756
0
-120736878

So by default this part :

forAll{(n:Int)=>

generates value from whole Integer range and our tests is because we are expecting natural numbers. To change this behaviour we need to create our own Generator

Generators

First let's try to use default Int generator. To do it we just need to use Arbitrary object

Arbitrary.arbitrary[Int]

and set proper constraints in it.

Arbitrary.arbitrary[Int].suchThat(i=>i>0)

So now code looks like this :

val natural: Gen[Int] =Arbitrary.arbitrary[Int].suchThat(i=>i>0)

  property("for every n>0 where n is Natural SUM(1,2...n)=n*(n+1)/2"){
      forAll(natural){(n:Int)=>
        println(n)
        MathLib.sumN(n) shouldBe ((1L to n).sum)
      }
  }

Notice that forAll now uses our generator. But is our test working now ?

Testing started at 18:11 ...
2147483647

TestFailedException was thrown during property evaluation.
  Message: -1073741824 was not equal to 2305843008139952128
  Location: (HelloPBTAnswerSpec.scala:18)
  Occurred when passed generated values (
    arg0 = 2147483647
  )

Unfortunately our mathematical formula is not prepared for integer overflow which may be the first bug we found in our library but for educational simplicity let's just assume that constraining integer values is also a good solution.

val natural: Gen[Int] =Gen.choose(0,1000)

  property("for every n>0 where n is Natural SUM(1,2...n)=n*(n+1)/2"){
      forAll(natural){(n:Int)=>
        println(n)
        MathLib.sumN(n) shouldBe ((1L to n).sum)
      }
  }

And now finally we have a passing test.

Exercise : write test for prepared functions

You have three functions to convert temperature degrees

val kelvinToCelsius: Kelvin=>Celsius= k => k - 273.15
val celsiusToFahrenheit:Celsius=>Fahrenheit = c => c * 9/5 + 32.0
val fahrenheitToKelvin :Fahrenheit=>Kelvin = f => (f+459.67) *5/9

Write PBT that is testing property : "(kelvin to celsius to fahrenheit to kelvin) should give first value"

compared values may differ max by +- 0.0001

Also notice how type aliases may be used in scala :

type Kelvin = Double
type Celsius = Double
type Fahrenheit = Double

ADDITIONAL EXERCISE

test property : "for every pair of non empty strings (s1.length + s2.length) > s1.length "

Testing Functions

Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/propertybased/exercises/TestingFunctionsSpec.scala

We know how to test values but how we can test functions and functional abstractions?

To gain knowledge on how to generate functions for property tests first of all we need to learn some new ways of creating Generators.

Generators with set of values

Following code will create generator which returns always on of 1,2,5 or 7

val simpleGenerator: Gen[Int] =Gen.oneOf(1,3,5,7)

And Another piece of code shows how to use existing generators to combine their results

val composedGenerator: Gen[Int] =Gen.oneOf(Gen.choose(0,100),Gen.choose(100,200))

Generators Composition

If we have two independent generators we can easily compose their value with simple for comprehension

  val pairsGenerator: Gen[(Int, Int)] = for{
    i1 <- simpleGenerator
    i2 <- composedGenerator
  } yield (i1,i2)

And an example usage would look like this

property("generators should properly generate pair of integers"){
      forAll(pairsGenerator){case (i1,i2) =>
        Set(1,3,5,7) should contain(i1)
        i2 should (be >= 0 and be <= 200)
      }
  }

Generating functions

Because function is a simple value then we can easily create generator with predefined set of functions

val intTointFunctions:Gen[Int=>Int]=Gen.oneOf((x:Int)=>x+1,(x:Int)=>x*2,(x:Int)=>x%10)
val stringToIntFunctions:Gen[String=>Int]=Gen.oneOf((s:String)=>s.toInt,(s:String)=>s.length)

And the usage

property("generator should generate a funcion"){
    forAll(intTointFunctions){ f:(Int=>Int) =>
      f shouldBe a [Function1[_,_]]
    }
  }

Exercise : write tests functions composition

Code : https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/propertybased/exercises/TestingFunctionsSpec.scala

  • implement two Property Tests
 //EXERCISES
 //MANDATORY
property("for every two functions f1 : String=> Int and f2 : Int=>Int , f1 andThen f2 should be equal to f2(f1(x))"){

}


property("for every two functions f1 : Int=> Int and f2 : String=>Int , f1 compose f2 should be equal to f1(f2(x))"){

}

ADDITIONALY

  • prove that Function.curry from standard library works properly
  //ADDITIONAL
  property("for every f:(Int,Int)=> Int , f.curry should generate equal function f` : Int=>Int=>Int"){

  }

Testing Domain

Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/propertybased/exercises/PBTDomainSpec.scala

Till now we were working within mathematical domain so now time for something closer to everyday problems.

Let's define a simple domain :

object PBTDomain {
  case class Price(value:BigDecimal)
  case class Product(name:String, price:Price)
  case class Purchase(id:Int,products:List[Product])


  trait PurchaseService{
    def purchase(purchase:Purchase)(p:Product) : Purchase = Purchase(purchase.id, p :: purchase.products)
  }

  object PurchaseService extends PurchaseService

}

Let's quickly check how to generate a domain object

val priceGenerator=for {
    i <- Gen.choose(0,Int.MaxValue)
  } yield Price(BigDecimal(i))

And again simple for comprehension is enough and usage is really trivial

property("price should be larger than 0"){
    forAll(priceGenerator){p=>
      p.value should be > BigDecimal(0)
    }
  }

Exercise : write domain generators

We already have generator for price. Now we need two more for product and purchase

  //Exercise
  property("product should have a name"){

  }

  property("purchase should not have empty list of products"){

  }

Additionally you can write Property Test for the Domain Service

 //ADDITIONAL
  property("PurchaseService should add product to product list in an existing purchase"){

  }

Testing Functional Abstractions

For this part of the workshop we will import two external libraries

libraryDependencies += "org.typelevel" %% "cats" % "0.4.1"
libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.1"

Both add additional Functional Programming mechanisms to standard Scala library. The plan is to use Property Based Testing to gain some knowledge and intuition around some interesting FP concepts

Monoid

Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/propertybased/exercises/PBTMonoidSpec.scala

According to wikipedia monoid is

To see how this concept can be used in practice take a look at the code in the following file : https://github.com/PawelWlodarski/workshops/blob/master/src/main/scala/jug/lodz/workshops/propertybased/PBTMonoids.scala

We have there examples of two monoids where S can be undertood as Integers but also there is a domain example where S is set of prices

Integers:

object IntAddMonoid extends Monoid[Int]{
    override def empty: Int = 0
    override def combine(x: Int, y: Int): Int = x+y
  }

Prices:

import PBTDomain.Price

  object PriceMonoid extends Monoid[Price] {
    override def empty: Price = Price(BigDecimal(0))
    override def combine(x: Price, y: Price): Price = Price(x.value+y.value)
  }

How to use this mechanism in real code? Below you have small reduce function.

def reduce[A](seq:Seq[A])(implicit monoid:Monoid[A]): A ={
    seq.fold(monoid.empty)(monoid.combine)
  }

Because price monoid is implicit we can treat it as a domain law or property of domain context

In other word it defines what is the domain meaning of reduce in the context of our domain. In this case it will calculate sum of all prices.

implicit val monoid=PriceMonoid

reduce(products.map(p=>p.price))

cats

In cats library Monoid is a part of algebra package

/**
 * A monoid is a semigroup with an identity. A monoid is a specialization of a
 * semigroup, so its operation must be associative. Additionally,
 * `combine(x, empty) == combine(empty, x) == x`. For example, if we have `Monoid[String]`,
 * with `combine` as string concatenation, then `empty = ""`.
 */
trait Monoid[@sp(Int, Long, Float, Double) A] extends Any with Semigroup[A] {

  /**
   * Return the identity element for this monoid.
   */
  def empty: A

and it extends Semigroup

/**
 * A semigroup is any set `A` with an associative operation (`combine`).
 */
trait Semigroup[@sp(Int, Long, Float, Double) A] extends Any with Serializable {

  /**
   * Associative operation taking which combines two values.
   */
  def combine(x: A, y: A): A

Exercise : test monoids

As Mandatory Part write test for both identity and asociativity properties of our second Int Monoid

 //EXERCISES
  property("IntMultiplyMonoid identity law"){

  }

  property("IntMultiplyMonoid asociativity law"){

  }

Additionally you can write test for PriceMonoid and our library reduce function.

  //ADDITIONAL
  property("PriceMonoid identity law"){

  }

  property("PriceMonoid asociativity law"){

  }

  property("reduce with Price Monoid should count sum of all prices"){

  }

Functor

Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/propertybased/exercises/PBTFunctorSpec.scala

In scala List can be treated as a functor and it gives us a possibility to transform List[String] into List[Int] with a function string=>string.length

To gain more intuition let's try to test if List actually respects Functor laws.

  • identity
 property("List satisfies Functor identity property"){
    forAll(notEmptyList){ ls=>
      ls.map(identity) shouldBe(identity(ls))
    }
  }
  • composition
 property("List satisfies Functor associativity property"){
    forAll(notEmptyList,stringToIntFunctions,intTointFunctions){ (ls,fsi,fii)=>
      ls.map(fsi andThen fii) shouldBe(ls.map(fsi).map(fii))
    }
  }

cats

To use Functor from Cats in practice we can redefine our reduce method so it adds another abstraction of how collection will be mapped

 def reduce[A,B](seq:List[A])(f:A=>B)(implicit monoid:Monoid[B], functor:Functor[List]): B ={
    functor.map(seq)(f).fold(monoid.empty)(monoid.combine)
  }

Implicit Functor[List] is defined somewhere in cats.std.all._

import cats.std.all._
implicit val monoid=PriceMonoid

val products= List(
  Product("tv",Price(BigDecimal(300))),
  Product("mouse",Price(BigDecimal(20)))
)

println(reduce(products)(p=>p.price))

Exercise : option as a Functor

 //EXERCISES
property("Option satisfies Functor identity property"){

}

property("Option satisfies Functor associativity property"){

}

Monad

Exercise File : https://github.com/PawelWlodarski/workshops/blob/master/src/test/scala/jug/lodz/workshops/propertybased/exercises/PBTMonadSpec.scala

Graphical Explanation of Monad is follwoing

So for example :

  • when we are calling database id=>Option[Value] and then we want to use a value to make another call Monad allow us to change result Option[Option[Value]] into Option[Value]
  • the same thing can be achieved when we want to call one webservice and then use the result to call another one Future[Future[Int]] -> Future[Int]

With given two simple functions

val split:String=>List[String] = s=>s.split(" ").toList
val variants:String=>List[String] = s=>List(s,s.toUpperCase,s.toLowerCase)

In our exercise we are going to test List agains three monad laws

  • Left identity
property("List satisfies left identity law : M.unit(a).flatMap(f) == f(a)"){
  forAll{(s:String)=>
      List(s).flatMap(split) shouldBe(split(s))
  }
}
  • Right identity
property("List satisfies right identity law : M.flatMap(M.unit) == M"){
    forAll{(ls:List[String])=>
      ls.flatMap(a=>List(a)) shouldBe(ls)
    }
  }
  • Associativity
property("List satisfies Associativity law : M.flatMap(f1).flatMap(f2) == M.flatMap(x=>f1(x).flatMap(f2))"){
    forAll{(ls:List[String])=>
      ls.flatMap(split).flatMap(variants) shouldBe(ls.flatMap(string=>split(string).flatMap(variants)))
    }
  }

Exercise : Option as a Monad

Her we will going to repeat Monad exercise for options and to make it more interesting some constructs from our Purchase Domain will be used.

//database
val purchaseWithId7=Purchase(7,List(
    Product("tv",Price(BigDecimal(300)))
  ))
  //mock DB
val purchases:Map[Int,Purchase] = Map(7 -> purchaseWithId7)

//curried general function
 val findPurchase : Map[Int,Purchase] => Int => Option[Purchase] = m => id => m.get(id)

//specific functions
 val findPurchaseInMockDB: (Int) => Option[Purchase] =findPurchase(purchases)
 val largest : List[Int] => Option[Int] = l => if(l.isEmpty) None else Some(l.max)

And the exercise

 property("Option satisfies left identity law : M.unit(a).flatMap(f) == f(a)"){
    //larges
  }

  property("Option satisfies right identity law : M.flatMap(M.unit) == M"){
    //no funcction needed
  }


  property("Option satisfies Associativity law : M.flatMap(f1).flatMap(f2) == M.flatMap(x=>f1(x).flatMap(f2))"){
    //use largest and find purchase in mock DB.
}

Monad Transformer

Example File : https://github.com/PawelWlodarski/workshops/blob/master/src/main/scala/jug/lodz/workshops/propertybased/PBTMonadTransformer.scala

What if for example we have two effects at the same time? Foe example when we are calling external webservice we have time - Future[T] and posibility that given value will be missing - Option

In this situation we can use predefined MonadTransformer . In Graphical explanation it will look like this :

Let's imagine that in the code we have two web services :

 def service1(name:String) : Future[Option[Int]] ...
 def service2(id:Int) : Future[Option[Company]]

We can not write simple for comprehension because argument of second web service Int is not the same as result of the first one Option[Int].

Solution with MonadTransformer

  import cats.data.OptionT
    import cats.std.future._

    val result=for{
      id<-OptionT(service1("company1"))
      company<-OptionT(service2(id))
    } yield company.info

    val finalValue: Future[Option[String]] = result.value

More here -> http://typelevel.org/cats/tut/optiont.html

Kleisli

Example File : https://github.com/PawelWlodarski/workshops/blob/master/src/main/scala/jug/lodz/workshops/propertybased/PBTKleisli.scala

Kleisli allow us to connect two so called monadic functions A=>M[B] and B=>M[C]

So a very quick example would be :

  import cats.data.Kleisli
  import cats.std.all._
  val split:String=>List[String] = s=>s.split(" ").toList
  val variants:String=>List[String] = s=>List(s,s.toUpperCase,s.toLowerCase)

  val variantsK = Kleisli(split).andThen(variants)

  println(variantsK.run("this is a sentence"))
  //List(this, THIS, this, is, IS, is, a, A, a, sentence, SENTENCE, sentence)

More info: http://typelevel.org/cats/tut/kleisli.html

And this was the last example from this workshop! Remember what was educational purpose of this material

  • Learn how to use Scalacheck
  • Gain better intuition around concept "Function as a Value"
  • Take quick glance at some very interesting Functional abstractions

Homework

  • You can check Basic Scalacheck course :https://pawelwlodarski.gitbooks.io/workshops/content/scalacheck.html
  • Try to finish every exercise from this workshop on your own. Play with examples and test additional possibilities of Scalacheck
  • Prove that According to Amdahl Law : S(n) = T(1) / T(n) = 1 / (B + (1 – B) / n) where
    • n - number of available threads
    • B- part of program which is sequential
    • then if B = 5% , you will receive max 20 time speedup when you have 1000 cores!!!

results matching ""

    No results matching ""