Skip to content

Commit

Permalink
Decouple cats-testkit from scalatest
Browse files Browse the repository at this point in the history
  • Loading branch information
rossabaker committed Aug 2, 2019
1 parent 0bab9eb commit 9ef3af0
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 47 deletions.
15 changes: 10 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,10 @@ lazy val docs = project
.settings(noPublishSettings)
.settings(docSettings)
.settings(commonJvmSettings)
.dependsOn(core.jvm, free.jvm, kernelLaws.jvm, laws.jvm, testkit.jvm)
.settings(libraryDependencies ++= Seq(
"org.typelevel" %%% "discipline-scalatest" % disciplineVersion
))
.dependsOn(core.jvm, free.jvm, kernelLaws.jvm, laws.jvm)

lazy val cats = project
.in(file("."))
Expand Down Expand Up @@ -562,6 +565,10 @@ lazy val tests = crossProject(JSPlatform, JVMPlatform)
.settings(moduleName := "cats-tests")
.settings(catsSettings)
.settings(noPublishSettings)
.settings(libraryDependencies ++= Seq(
"org.scalatestplus" %%% "scalatestplus-scalacheck" % scalatestplusScalaCheckVersion,
"org.typelevel" %%% "discipline-scalatest" % disciplineVersion
))
.jsSettings(commonJsSettings)
.jvmSettings(commonJvmSettings)
.settings(scalacOptions in Test := (scalacOptions in Test).value.filter(_ != "-Xfatal-warnings"))
Expand All @@ -576,9 +583,7 @@ lazy val testkit = crossProject(JSPlatform, JVMPlatform)
.settings(disciplineDependencies)
.settings(
libraryDependencies ++= Seq(
"org.scalatest" %%% "scalatest" % scalatestVersion,
"org.scalatestplus" %%% "scalatestplus-scalacheck" % scalatestplusScalaCheckVersion,
"org.typelevel" %%% "discipline-scalatest" % disciplineVersion
"org.scalacheck" %%% "scalacheck" % scalaCheckVersion
)
)
.jsSettings(commonJsSettings)
Expand Down Expand Up @@ -613,7 +618,7 @@ lazy val alleycatsLaws = crossProject(JSPlatform, JVMPlatform)

lazy val alleycatsTests = crossProject(JSPlatform, JVMPlatform)
.in(file("alleycats-tests"))
.dependsOn(alleycatsLaws, testkit % "test")
.dependsOn(alleycatsLaws, tests % "test-internal -> test")
.settings(moduleName := "alleycats-tests")
.settings(catsSettings)
.settings(noPublishSettings)
Expand Down
85 changes: 43 additions & 42 deletions docs/src/main/tut/typeclasses/lawtesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,19 @@ section: "typeclasses"
# Law testing

[Laws](https://typelevel.org/cats/typeclasses.html#laws) are an important part of cats.
Cats uses [discipline](https://github.com/typelevel/discipline) to define type class laws and
Cats uses [discipline](https://github.com/typelevel/discipline) to define type class laws and
the [ScalaCheck](https://github.com/rickynils/scalacheck) tests based on them.

To test type class laws from Cats against your instances, you need to add a `cats-laws` dependency.
If you are using `ScalaTest`, Cats also ships with optional `cats-testkit`, which provites a convenient
base test class `CatsSuite`.

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 (or `cats-testkit` if you
are using `ScalaTest`).
First up, you will need to specify dependencies on `cats-laws` in your `build.sbt` file.
To make things easier, we'll also include the `scalacheck-shapeless` library in this tutorial, so we don't have to manually write instances for ScalaCheck's `Arbitrary`.

```scala
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-laws" % "1.1.0" % Test, //or `cats-testkit` if you are using ScalaTest
"org.typelevel" %% "cats-laws" % "1.1.0" % Test,
"com.github.alexarchambault" %% "scalacheck-shapeless_1.13" % "1.1.6" % Test
)
```
Expand Down Expand Up @@ -52,9 +48,9 @@ object Tree {
import Tree._ //there is no real companion object in REPL
```

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

```scala
Expand All @@ -74,72 +70,73 @@ but you won't need it if you import `org.scalacheck.ScalacheckShapeless._`).
import org.scalacheck.{Arbitrary, Gen}
implicit def arbFoo[A: Arbitrary]: Arbitrary[Tree[A]] =
Arbitrary(Gen.oneOf(Gen.const(Leaf), (for {
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](https://github.com/typelevel/discipline) provides a helper `checkAll` function that performs
this conversion for two test frameworks: `ScalaTest` and `Specs2`.

Now we can convert these `ScalaCheck` `Properties` into tests that the test framework can run.

[discipline](https://github.com/typelevel/discipline) provides two helper `checkAll` functions that perform
this conversion for two test frameworks: `ScalaTest` and `Spec2`.
* If you are using `Specs2`, extend your test class with `org.typelevel.discipline.specs2.Discipline`.

If you are using `Specs2`, let the test class extend `org.typelevel.discipline.specs2.Discipline`
which provides the `checkAll` function.
* If you are using `ScalaTest`, extend your test class with `org.typelevel.discipline.scalatest.Discipline` and `org.scalatest.funsuite.AnyFunSuiteLike`.

If you are using `ScalaTest`, let the test class extend `org.typelevel.discipline.scalatest.Discipline`, or
inherit from the more convenient `cats.tests.CatsSuite` from `cats-testkit`.
`CatsSuite` extends the standard ScalaTest `FunSuite`, `Matchers` together with
`org.typelevel.discipline.scalatest.Discipline`. Furthermore it also pulls in all of cats instances and syntax,
so there's no need to import from `cats.implicits._`.
* For other test frameworks, you need to resort to their integration with `ScalaCheck` to test
the `ScalaCheck` `Properties` provided by cats-laws.

For other test frameworks, you need to resort to their integration with `ScalaCheck` to test
the `ScalaCheck` `Properties` provided by cats-laws.

So here is the Scalatest test for our example, basically we import `cats.laws.discipline.FunctorTests` and
call the `checkAll` helper with it.
The following example is for ScalaTest. If not using

```tut:book
import cats.tests.CatsSuite
import cats.implicits._
import cats.laws.discipline.FunctorTests
import org.scalatest.funsuite.AnyFunSuite
import org.typelevel.discipline.scalatest.Discipline
import arbitraries._
class TreeLawTests extends CatsSuite {
class TreeLawTests extends AnyFunSuite with Discipline {
checkAll("Tree.FunctorLaws", FunctorTests[Tree].functor[Int, Int, String])
}
```

* `cats.implicits._` imports the instances we need for `Eq[Tree[Int]]`, which the laws use to compare trees.
* `FunctorTests` contains the functor laws.
* `AnyFunSuite` defines the style of ScalaTest.
* `Discipline` provides `checkAll`, and must be mixed into `AnyFunSuite`
* `arbitraries._` imports the `Arbitrary[Tree[_]]` instances needed to check the laws.

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
[info] - Tree.FunctorLaws.functor.covariant identity
[info] - Tree.FunctorLaws.functor.invariant composition
[info] - Tree.FunctorLaws.functor.invariant identity
[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] ScalaTest
[info] Run completed in 537 milliseconds.
[info] Run completed in 1 second, 362 milliseconds.
[info] Total number of tests run: 4
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 4, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[info] Passed: Total 4, Failed 0, Errors 0, Passed 4
[success] Total time: 1 s, completed Aug 31, 2017 2:19:22 PM
[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
### 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.
Let's test it out by defining a `Semigroup` instance for our `Tree` type.

```tut:book
import cats.implicits._
Expand All @@ -154,13 +151,17 @@ implicit def semigroupTree[A: Semigroup]: Semigroup[Tree[A]] = new Semigroup[Tre
}
```

Then we can again test the instance inside our class extending `CatsSuite`:
Then we can add the Semigroup tests to our suite:

```tut:book
import cats.laws.discipline.FunctorTests
import cats.implicits._
import cats.kernel.laws.discipline.SemigroupTests
import cats.laws.discipline.FunctorTests
import org.scalatest.funsuite.AnyFunSuite
import org.typelevel.discipline.scalatest.Discipline
import arbitraries._
class TreeLawTests extends CatsSuite {
class TreeLawTests extends AnyFunSuite with Discipline {
checkAll("Tree[Int].SemigroupLaws", SemigroupTests[Tree[Int]].semigroup)
checkAll("Tree.FunctorLaws", FunctorTests[Tree].functor[Int, Int, String])
}
Expand Down

0 comments on commit 9ef3af0

Please sign in to comment.