Image credit easysolutionweb.com
In this post Laurence Bird covers how to use the power of property based testing and automatic type class derivation, with the end goal of making your tests more concise and less prone to bugs. This article was orignally found on OVO Enegy Tech Blog and is written by Laurence Bird.
"A wise man once told me “never try and explain more than one concept in a blog post”, this christmas I’m throwing caution to the wind and doing just that. Stay with me and this blog post I'm going cover how to use the power of property based testing and automatic type class derivation, with the end goal of making your tests more concise and less prone to bugs.
Type classes
Scalacheck
import org.scalacheck.Prop.forAll
val propReverseList = forAll { l: List[String] => l.reverse.reverse == l }
What the above code is doing within the forAll block, is generating a random list of strings, reversing it twice and ensuring that the original ordering of the list is preserved. The code within the forAll block is executed a configurable number of times, with an assertion which is evaluated to determine the test result.
One of the things I really like about this is it removes the need for ever growing utility objects for testing, which consistently need to be updated as case classes evolve over time. Under the hood scalacheck uses an implicit instance of a type class called Arbitrary to configure the generated Arbitrary values for any generic type T which looks like this:
sealed abstract class Arbitrary[T] extends Serializable {
val arbitrary: Gen[T]
}
That is to say for an instance of a type class, we need a Gen (generator) to generate an instance of this type. Scalacheck provides Arbitrary type class instances for most standard scala types out the box (which can be found here), along with helper methods to create generators (as seen here).
One important feature to be noted here is that an arbitrary needs to be defined for all types and not just primatives. This means as your code base grows and your tests get more complex, as do the Arbitrary values which you need for your tests to function. Arbitrary type classes can be composed, however for large codebases there can be some of boilerplate involved. Those dreaded test utility packages which were once filled with endless varieties of case class instances are now filled with Arbitrary type class instances, which if you're not careful can continually grow.
However if we consider the following case class:
case class Gift(name: String, cost: Long, currency: Currency)
If we have arbitrary values defined for types String, Long and Currency, logically speaking there should be away we can combine these into a single arbitrary instance of Gift without having to hand write a new Arbitrary instance? Luckily we can do just that, using automatic type class derivation: enter Magnolia.
Magnolia
Magnolia abstracts over scala Macros to allow users to generate type classes for case classes, and sealed trait hierarchies. On a high level, it will generate type class instances for you by automagically combining defined primitive type class instances at compile time to a composite type class. This same thing can be achieved using shapeless, however Magnolia distinguishes itself by claiming to offer between 4x and 15x performance improvement. Very exciting I know, let's jump into an example of what a basic derivation object for an Arbitrary in magnolia would look like:
import magnolia._
import scala.language.experimental.macros
import org.scalacheck.Arbitrary
trait ArbDerivation {
type Type class[T] = Arbitrary[T]
def combine[T](caseClass: CaseClass[Type class, T]): Type class[T] = ???
def dispatch[T](sealedTrait: SealedTrait[Type class, T]): Type class[T] = ???
implicit def gen[T]: Type class[T] = macro Magnolia.gen[T]
}
The combine method will provide the code we need to combine the primitive type class instances which make up all the constructor parameters of a case class, to a single type class instance for that given case class. And the dispatch method will allow us to determine the appropriate type class to use for a sealed trait hierarchy.
Lets move our focus to the combine method, from this we need to derive an Arbitrary[T] from all the parameters on a given CaseClass[Type class, T]. Luckily CaseClass exposes a method which allows us to do just that:
def construct[Return](makeParam: Param[Type class, Type] => Return): Type
To use this method we pass an anonymous function, which defines how to construct an instance of any parameter on a case class from its as associated type class (which in the context of this given example Arbitrary). We can use this to summon an instance of a given case class, from all the Arbitrary type class instances we have in scope of its primatives. At compile time then Magnolia expand this using macros to generate an instance of the Arbitrary type class which represents the whole case class being handled (the source code can be found [here]).
Now I know thats a lot of information, so lets see what makeParam function we would pass to the construct method defined above, in order to construct a value Type, from it's respective type class instance Arbitrary[Type] would look like:
def makeParam[Type](param: Param[Type class, Type]) = {
param
.type class
.arbitrary
.pureApply(
Parameters.default,
Seed.random()
)
}
Parameters and seeds in the context of above are simply configuration settings for scalacheck to determine the size of the generated item and the random number generation settings. So this is the makeParam function we need to pass to Magnolia to construct an instance of of a given case class. Lets go back to the original derivation object we saw previously and see where we have got to now:
package com.ovovenergy.christmas
import magnolia._
import org.scalacheck.Gen.Parameters
import org.scalacheck.rng.Seed
import org.scalacheck.{Arbitrary, Gen}
import scala.language.experimental.macros
trait ArbDerivation {
implicit def parameters: Parameters
implicit def gen[T]: Arbitrary[T] = macro Magnolia.gen[T]
type Type class[T] = Arbitrary[T]
def combine[T](ctx: CaseClass[Arbitrary, T]): Arbitrary[T] = {
val t: T = ctx.construct { param: Param[Type class, T] =>
param
.type class
.arbitrary
.pureApply(parameters, Seed.random())
}
Arbitrary(Gen.delay(t))
}
}
You can see in the final step, we wrap our value of T in an Arbitrary, and voilà! We can now automatically derive composite Arbitrary type classes for any case class, given we have Arbitrary instances for its primitive types in scope. It's worth noting in this case the Gen.delay, ensures that the arbitrary instance is evaluated lazily and each time it is summoned rather than once. Otherwise this would result in the same value being generated continually for each test case.
For fear of information overload I haven't included an explanation of the derivation of sealed traits, but if you're interested the code for this and the rest of the example covered in this post can be found in this example project.
Conclusion
Although a lot has been covered in this post, when you actually go and look at the code required for this derivation it's very concise, surpisingly readable and the compile time is negligible. There isn't a tonne of documentation out there but thats partly because there aren't that many concepts which need to be covered, by reading through their examples on git, and the tutorial it's very achievable to be deriving your own type classes in no time.
It's worth noting that Magnolia is still an experimental library, run time performance hasn't been evaluated and as stated in their documentation it hasn't had the same exposure to real-world datatypes that Shapeless has had. Overall however I've had fun working with it, and the compile time to magic ratio (CMR) makes it a compelling tool to utilise in future projects."
Article found on OVO Energy Tech Blog. Written by Laurence Bird.