Your test framework supports before/after hooks, don't use them.
Most test frameworks have a concept of hooks. These hooks deal
with aspects of setting up the situation where a test can run,
and then cleaning that situation up. These hooks are commonly
called before
and after
with plenty of
different names, modifiers, and behaviors between
implementations. They are often declared and execute like this:
before(() => {
createDatabase()
})
testCase('this is a test', () => {
// assert something dependent on the database
})
testCase('this is another test', () => {
// assert something dependent on the database
})
after(() => {
destroyDatabase()
})
While common to test frameworks and interesting to implement, hooks ought to be avoided in tests. Instead of hooks, prefer to use plain language constructs such as functions to serve the same purpose:
function withDatabase(func) {
createDatabase()
func()
destroyDatabase()
}
testCase('this is a test', () => {
withDatabase(() => {
// assert something dependent on the database
})
})
testCase('this is another test', () => {
withDatabase(() => {
// assert something dependent on the database
})
})
There are many significant benefits to using functions over hooks:
We get improved code locality because everything we need to understand the test case is in the test case. Not only do test frameworks support before/after, they support multiple before/after hooks. This can get pretty crazy as you can find code with hooks shared and registered across many files.
We use less magic. Everyone has a baseline understanding of functions, methods, and calling order. Hooks like before/after can and are implemented in crazy ways. Minimizing the mental burden of writing against a specific test framework is advantageous.
We can do proper composition of code. Hooks
like before/after aren't good for organizing and reusing
code because they can only be registered with the test
framework. This inversion of control to the framework is
very limiting. We can't enforce
beforeB
requires beforeA
or
afterC
must be called after
beforeC
; this is all implicit. Functions on the
other hand compose so we can do these things plainly:
const setupB = () => {
setupA()
// do set up dependent on A
}
const withC = (fn) => {
setupC()
fn()
tearDownC()
}
No extra work is done for a test case that
doesn't need it. Very often some tests will require more
convoluted setups than others and tests sharing that
before
will pay a price. This extra work is
wasteful, but also usually noise that muddles what is
intended to be under test.
We don't have to restate the facts of what
happens in the plain function. If you're working in a typed
language, you'll often feel the friction of asserting that
the before
hook did the setup as expected.
Because it was done in a far-off place, the type system
cannot help make the connections. This leads to adding more
noise to satisfy the typechecker or annotations to opt-out
of types, making the tests get less out of typing.
Beyond types, asserting side-effects have occurred also can
suffer from this first-verify-before-then-test issue. This
happens very naturally when there's an implicit assumption
about how the before
should run. Since
before
is shared and may be refactored, we may
need some verification to
test the before
is doing the right
thing. Being explicit with functions, we don't need this
defensive checking.
We don't have a dumping ground for
unrelated code. Hooks put negative social pressure on the
growth of the code. Using before
may feel nice:
test cases are shorter because all the work is in
before
! While true, this also means
all the work is in before
. As new code is added, people most often repeat
established patterns. This leads to more and more code
dumped into before
, it becomes a junk drawer
which exacerbates all the above issues.
Even more problems exist with different implementations of hooks. We can't cover all the issues that only some test frameworks exhibit, but rest assured this list is not exhaustive. Functions, on the other hand, have much more portable semantics and are more generally applicable.