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.