kallikrein is a Scala testing framework for sbt focused on running cats-effect based programs.
If you're into matcher DSLs, check out xpct, which is a framework-agnostic typed matcher lib with support for kallikrein.
"io.tryp" %% "kallikrein-sbt" % "0.5.2"
"io.tryp" %% "kallikrein-http4s-sbt" % "0.5.2"
To use the framework in a project, specify the setting:
testFrameworks += new TestFramework("klk.KlkFramework")
class SomeTest
extends klk.IOTest
{
test("description")(IO.pure(1 == 1))
}
Tests are added by calling the test
function in a class inheriting klk.SimpleTest[F]
, where F[_]
is an effect that
implements cats.effect.Sync
.
klk.IOTest
is a convenience trait using cats.effect.IO
.
Assertions are returned from the test thunk and can be anything, as long as there is an instance for klk.TestResult
.
The internal type representing the result is KlkResult
.
The above mentioned test
builder can also be used in a pure context and has a nice arsenal of typeclass instances for
composition.
When tests are sequenced in a for comprehension, the semantic effect is that of conditional execution: If one test fails, all following tests are skipped.
There is an instance of SemigroupK
available, allowing you to use the <+>
operator, resulting in the alternative, or
unless
, semantics – i.e. if and only if the first test fails, execute the second one and use its result.
For independent tests, there are two combinators: sequential
and parallel
.
They do what you would expect, similar to the imperative test building syntax.
The parallel
variant requires an instances of cats.Parallel
and will execute the tests with a parTraverse
.
The following example will result in a successful end result:
class DepTest
extends ComposeTest[IO, SbtResources]
{
def testSuccess: IO[Boolean] =
IO.pure(true)
def testFail: IO[Boolean] =
IO.raiseError(new Exception("boom"))
implicit def cs: ContextShift[IO] =
IO.contextShift(ExecutionContext.global)
def tests: Suite[IO, Unit, Unit] =
for {
_ <- sharedResource(Resource.pure(5))(
builder =>
builder.test("five is 4")(five => IO.pure(five == 4)) <+>
builder.test("five is 5")(five => IO.pure(five == 5))
)
_ <- test("test 1")(testSuccess)
_ <- test("test 2")(testFail) <+> test("test 3")(testSuccess)
_ <- Suite.parallel(test("test 4a")(testSuccess), test("test 4b")(testSuccess)) <+> test("test 5")(testFail)
_ <- test("test 7")(testSuccess)
} yield ()
}
The effect type of an individual test can be different from the main effect if there is an instance of klk.Compile[F, G]
.
For example, EitherT
is supported out of the box:
test("EitherT")(EitherT.right[Unit](IO.pure(1 == 1)))
A Left
value will be converted into a failure by the typeclass TestResult[A]
, meaning that this works just as well with
IO[Either[A, B]]
.
A Stream[F, A]
will automatically be compiled to F
, with the inner value being handled by a dependent typeclass
instance.
The kallikrein-http4s-sbt
module provides a shared resource that runs an http4s server on a random
port and supplies tests with a client and the Uri
of the server wrapped in a test client interface:
class SomeTest
extends klk.Http4sTest[IO]
{
def tests: Suite[IO, Unit, Unit] =
server
.app(HttpApp.liftF(IO.pure(Response[IO]())))
.test { builder =>
builder.test("http4s") { client =>
client.fetch(Request[IO]())(_ => IO.pure(true))
}
}
}
The TestClient
class provides a fetch
method that injects the uri into the request with its
withUri(request: Request[F])
method, as well as a success
method, which produces a failed KlkResult
if the
response status is other than 2xx
.
Tests can depend on shared and individual resources that will be supplied by the framework when running:
class SomeTest
extends klk.IOTest
{
val res1: Resource[IO, Int] = Resource.pure(1)
val res2: Resource[IO, Int] = Resource.pure(1)
test("resource").resource(res1).resource(res2)((i: Int) => (j: Int) => IO.pure(i == j))
def eightySix: SharedResource[IO, Int] =
sharedResource(Resource.pure(86))
eightySix.test("shared resource 1")(i => IO.pure(i == 86))
eightySix.test("shared resource 2")(i => IO.pure(i == 68))
}
The shared resource will be acquired only once and supplied to all tests that use it.
Scalacheck can be used in a test by calling the forall
method on a test builder:
class SomeTest
extends klk.IOTest
{
test("are all lists of integers shorter than 5 elements?").forall((l1: List[Int]) => IO(l1.size < 5))
}
This features a custom runner for the properties built on fs2.
A second variant forallNoShrink
does what it advertises.
cats-discipline laws can be checked like this:
class SomeTest
extends klk.IOTest
{
test("laws").laws(IO.pure(FunctorTests[List].functor[Int, Int, Int]))
}