Tagless with Discipline — Testing Scala Code The Right Way

This will allow us to test with sufficient confidence that any implementation is following our laws.Thus, we get the following benefits:We do not have to write tests — only laws and some infrastructure code (data generators, equality definitions).Tests can exercise cases that are hard to come by when writing them manually (e.g., very large strings, empty values).Tests work regardless of implementation.Laws serve as an explicit documentation of behavior.Let’s see the details!Writing LawsWhen you take a look at the Emails algebra, the following laws come to mind:For every saved email e, find(e) returns e.For every saved email e, known(e) returns true.find is consistent with known i.e., find(e) is defined IFF known(e) is true.Saving the same email twice always returns EmailAlreadyExists error.By translating these laws into operations using this algebra, you get (in pseudo-code):save(e) >> findEmail(e) <-> pure(Some(e))save(e) >> known(e) <-> pure(true)findEmail(e).fmap(_.isDefined) <-> known(e)save(e) *> save(e) <-> pure(Left(EmailAlreadyExists))In the example above, we use the standard cats syntax where: a >> b means a flatMap (_ => b); a *> b means product(a, b).map(_._2)..We also use the <-> symbol to express the equivalent to relation.Similarly, you can devise a set of laws for Users algebra:For every created user u, identifyUser(primaryEmail(u)) returns u.For every created user u, identifyUser(e) returns u IFF e has been attached to the user u.For every user u with profile p, creating the user and then updating their profile is equivalent to creating the user with the profile already updated i.e., createUser(e, p) >>= (u => updateUserProfile(uid(u), f(p))) <-> createUser(e, f(p)).Attaching n emails via n calls to attachEmail is equivalent to calling attachEmails once with collection of all n-emails.To be complete, we should have written laws governing the behavior of the remaining methods — find, getEmails, etc….I took the liberty of skipping it to be concise.Let’s see how we implement these laws in Discipline.Implementing LawsThe implementation of law checking needs to be tailored to ScalaCheck to achieve automated testing..That is, a law must be a valid ScalaCheck property..We’ll be using cats-kernel-laws provided IsEq type for that..The purpose of this type is twofold..First, IsEq(lhs, rhs) states that the left-hand side of the IsEq expression is equivalent to its right-hand side..Second, it is convertible to ScalaCheck Prop by Discipline..We form IsEq instances by using a handy <-> operator.So, the implementation of the first law for the Emails algebra might look like this:The way we read the method is:For any email: Email, the expression algebra.save(email) >> algebra.findEmail(email) must be equivalent to M.pure(Some(email))..And that's what we want every implementation of the Email algebra to respect.We’ll also prepare a generic test suite (called Laws in Discipline's terminology):Now, we have to tell ScalaCheck how to generate emails..I recommend reading a ScalaCheck tutorial first, but it’s very simple in essence..There has to be an Arbitrary[Email] instance in the implicit scope of tests:Finally, we’ll have to specify how to check the equivalence for a given monad M.. More details

Leave a Reply