testz-resource
Inside testz-resource is a couple of models for allocating and cleaning up resources that are shared between tests.
These come in the form of a couple of harness types; firstly, ResourceHarness
.
abstract class ResourceHarness[T[_]] { self =>
def test[R](name: String)(assertions: R => Result): T[R]
def namedSection[R](name: String)(test1: T[R], tests: T[R]*): T[R]
def section[R](test1: T[R], tests: T[R]*): T[R]
def allocate[R, I]
(init: () => I)
(tests: T[(I, R)]): T[R]
}
Noting the differences between ResourceHarness[T[_]]
and Harness[T]
:
ResourceHarness
requires a type constructor, unlikeHarness
which takes a fully-saturated type parameter.ResourceHarness
has atest
method which takes anR => Result
, as opposed toHarness.test
which takes an() => Result
. This expresses that the test depends on a resource of typeR
.ResourceHarness.test
also returns aT[R]
, not aT
. This is because the typeT[R]
represents a group of tests which depend on a resource of typeR
.ResourceHarness.namedSection
andResourceHarness.section
are both polymorphic overR
and take inT[R]
’s and return aT[R]
, whereasHarness.namedSection
andHarness.section
take in and returnT
’s.allocate
is a new method onResourceHarness
; it lets you discharge one resource obligation by providing a way to allocate that resource. That’s why it takes in aT[(I, R)]
and() => I
, and returns aT[R]
, which is a test group.
T[_]
is likely to be a contravariant functor, because what it represents is a consumer
of R
values.
To type a group of tests with all resources accounted for -
including the case of no resources needed at all - the type T[Unit]
suffices.
allocate
can be used to “fill in” one of the resources required by
a group of tests, by describing how the resource is allocated.
Resources should be allocated immediately before all tests within an allocate
block
execute. After all tests within an allocate
block execute, the resource being
allocated should no longer be referenced.
The intent behind allocate
is to keep test data short-lived; data which
is live for a long time is promoted to the old generation, making garbage collection
much more expensive.
Here’s an example:
import testz._
object TestsWithResources {
def tests[T[_]](harness: ResourceHarness[T]): T[Unit] = {
import harness._
section(
allocate(() => List(1, 2, 3, 4))(
test("the list should be ascending") {
case (list, _) =>
assert(list == list.sorted)
}
),
test("doesn't see the list, it's not referenced by the time the test executes") {
case () =>
assert(true)
}
)
}
}
This code makes sure to be careful about resources; though I’m only using a List
here, large buffers and the like are useful with ResourceHarness
, to avoid keeping
too much data in memory and tightly control references.
You may (rightly) ask: what about resources which require cleanup?
Well, if you want to use a resource that needs cleaning up, you definitely want more
than R => Result
; you want R => F[Result]
for some F[_]
. Hence,
EffectResourceHarness
:
trait EffectResourceHarness[F[_], T[_]] { self =>
def test[R]
(name: String)
(assertions: R => F[Result]): T[R]
def namedSection[R]
(name: String)
(test1: T[R], tests: T[R]*
): T[R]
def section[R]
(test1: T[R], tests: T[R]*
): T[R]
def bracket[R, I]
(init: () => F[I])
(cleanup: I => F[Unit])
(tests: T[(I, R)]
): T[R]
}
The difference between ResourceHarness.allocate
and EffectResourceHarness.bracket
is that bracket
takes some cleanup action as well, which returns an F[Unit]
, and
actually allocating the value can execute an effect in F[_]
as well.
EffectResourceHarness
has similarly tight guarantees; allocation happens before
the tests in bracket
, cleanup happens immediately after the tests in bracket
.