testz-core
testz-core is the bare bones of testz.
As such, the source should be fully explainable, which I attempt to do below.
The type of test results in testz is Result.
Result is a glorified Boolean; either Fail() or Succeed().
It’s encoded as an ordinary ADT, with an encoding which provides slightly nicer type inference.
A delayed computation of type Result (e.g. () => Result) is often called
a “test function”.
sealed abstract class Result
// this constructor is `private` in the real code, but in the documentation
// every statement is separate in the REPL so `Fail` is not the companion of `Fail`
// and the constructor is *never* accessible.
final class Fail() extends Result {
override def toString(): String = "Fail()"
override def equals(other: Any): Boolean = other.asInstanceOf[AnyRef] eq this
}
object Fail {
private val cached = new Fail()
def apply(): Result = cached
}
// this constructor is `private` in the real code, but in the documentation
// every statement is separate in the REPL so `Succeed` is not the companion of `Succeed`
// and the constructor is *never* accessible.
final class Succeed() extends Result {
override def toString(): String = "Succeed()"
override def equals(other: Any): Boolean = other.asInstanceOf[AnyRef] eq this
}
object Succeed {
private val cached = new Succeed()
def apply(): Result = cached
}
object Result {
def combine(first: Result, second: Result): Result =
if (first eq second) first
else Fail()
}
Let’s move on to Harness[T]. Harness[T] is used to write most unit test suites, and can be
implemented in quite a few ways for different featueres. A typical way to use it is by writing
methods that take Harness[T] for any T.
Harness[T] provides hierarchical test registration with methods for
operating on and creating values of type T. Think of T values as trees
with (optionally) strings at the nodes and test functions at the leaves.
It provides a method test(String)(() => Result): T which registers
a test under a name, returning a “test group value” of type T, where
a test is a () => Result; a function with no parameters that computes
a testz.Result (described later in this file)
It also provides a method section(T, T*) which takes one or more “test groups”
and returns all of them wrapped in a new unnamed group. To name the new test group,
use the namedSection(String)(T, T*) method.
abstract class Harness[T] {
def test(name: String)(assertions: () => Result): T
def namedSection(name: String)(test1: T, tests: T*): T
def section(test1: T, tests: T*): T
}
By slightly extending Harness to allow tests to execute effects, we get
EffectHarness. For example, a test suite passed a EffectHarness[Future, T]
can register tests that have asynchronous results.
abstract class EffectHarness[F[_], T] {
def test(name: String)(assertions: () => F[Result]): T
def namedSection(name: String)(test1: T, tests: T*): T
def section(test1: T, tests: T*): T
}
EffectHarness can be made into Harness by calling EffectHarness.toHarness; given a way to translate
a Result to an F[Result], you can create an EffectHarness[F, T] from a Harness[T].
Phrased differently: given a “default” translation of Result to F[Result], toHarness is a “default”
translation of EffectHarness[F, T] to Harness[T].
object EffectHarness {
def toHarness[F[_], T](
self: EffectHarness[F, T]
)(
pure: Result => F[Result]
): Harness[T] = new Harness[T] {
def test
(name: String)
(assertions: () => Result)
: T = self.test(name)(() => pure(assertions()))
def namedSection
(name: String)
(test1: T, tests: T*)
: T = self.namedSection(name)(test1, tests: _*)
def section
(test1: T, tests: T*)
: T = self.section(test1, tests: _*)
}
}
That’s all there is to testz-core; the next step in the test-writing side of testz is testz-resource.
However, if you’re more interested in testz’s implementation, you can skip to testz-stdlib.