Law testing

Laws are an important part of cats. Cats uses discipline to define type class laws and the ScalaCheck tests based on them.

To test type class laws from Cats against your instances, you need to add a cats-laws dependency.

Getting started

First up, you will need to specify dependencies on cats-laws in your build.sbt file.

libraryDependencies ++= Seq(
  "org.typelevel" %% "cats-laws" % "2.12.0" % Test,
)

Example: Testing a Functor instance

We'll begin by creating a data type and its Functor instance.

import cats._

sealed trait Tree[+A]
case object Leaf extends Tree[Nothing]
case class Node[A](p: A, left: Tree[A], right: Tree[A]) extends Tree[A]

object Tree {
  implicit val functorTree: Functor[Tree] = new Functor[Tree] {
    def map[A, B](tree: Tree[A])(f: A => B) = tree match {
      case Leaf => Leaf
      case Node(p, left, right) => Node(f(p), map(left)(f), map(right)(f))
    }
  }
}

Cats defines all type class laws tests in cats.laws.discipline.* as discipline's RuleSets. Each RuleSet provides a ScalaCheck Properties through ruleSet.all to represent all the rules that it defines and inherits. For example, the ScalaCheck Properties for Functor can be retrieved using

cats.laws.discipline.FunctorTests[Tree].functor[Int, Int, String].all

We will also need to create an Eq instance, as most laws will need to compare values of a type to properly test for correctness. For simplicity we'll just use Eq.fromUniversalEquals:

implicit def eqTree[A: Eq]: Eq[Tree[A]] = Eq.fromUniversalEquals

ScalaCheck requires Arbitrary instances for data types being tested. We have defined an Arbitrary instance for Tree here, if you use Scala 2, you can avoid writing it manually with scalacheck-shapeless.


import org.scalacheck.{Arbitrary, Gen}

object arbitraries {
  implicit def arbTree[A: Arbitrary]: Arbitrary[Tree[A]] =
    Arbitrary(Gen.oneOf(Gen.const(Leaf), (for {
      e <- Arbitrary.arbitrary[A]
    } yield Node(e, Leaf, Leaf)))
  )
}

Now we can convert these ScalaCheck Properties into tests that the test framework can run. discipline provides a helper checkAll function that performs this conversion for three test frameworks: ScalaTest, Specs2 and MUnit.

The following example is for MUnit.

import cats.syntax.all._
import cats.laws.discipline.FunctorTests
import munit.DisciplineSuite
import arbitraries._

class TreeLawTests extends DisciplineSuite {
  checkAll("Tree.FunctorLaws", FunctorTests[Tree].functor[Int, Int, String])
}

Now when we run test in our sbt console, ScalaCheck will test if the Functor laws hold for our Tree type. You should see something like this:

[info] TreeLawTests:
[info] - Tree.FunctorLaws.functor.covariant composition (58 milliseconds)
[info] - Tree.FunctorLaws.functor.covariant identity (3 milliseconds)
[info] - Tree.FunctorLaws.functor.invariant composition (19 milliseconds)
[info] - Tree.FunctorLaws.functor.invariant identity (3 milliseconds)
[info] Passed: Total 4, Failed 0, Errors 0, Passed 4
[success] Total time: 2 s, completed Aug 2, 2019 12:01:17 AM

And voila, you've successfully proven that your data type upholds the Functor laws!

Testing cats.kernel instances

For most of the type classes included in Cats, the above will work great. However, the law tests for the type classes inside cats.kernel are located in cats.kernel.laws.discipline.* instead. So we have to import from there to test type classes like Semigroup, Monoid, Group or Semilattice.

Let's test it out by defining a Semigroup instance for our Tree type.

import cats.syntax.all._

implicit def semigroupTree[A: Semigroup]: Semigroup[Tree[A]] = new Semigroup[Tree[A]] {
  def combine(x: Tree[A], y: Tree[A]) = (x, y) match {
    case (Leaf, _) => Leaf
    case (_, Leaf) => Leaf
    case (Node(xp, xLeft, xRight), Node(yp, yLeft, yRight)) =>
      Node(xp |+| yp, combine(xLeft, yLeft), combine(xRight, yRight))
  }
}

Then we can add the Semigroup tests to our suite:

import cats.syntax.all._
import cats.kernel.laws.discipline.SemigroupTests
import cats.laws.discipline.FunctorTests
import munit.DisciplineSuite
import arbitraries._

class TreeLawTests extends DisciplineSuite {
  checkAll("Tree.FunctorLaws", FunctorTests[Tree].functor[Int, Int, String])
  checkAll("Tree[Int].SemigroupLaws", SemigroupTests[Tree[Int]].semigroup)
}