test-framework

Homepage: https://github.com/epsil/test-framework.el

Author: Vegard Øye

Updated:

Summary

Framework for unit testing

Commentary

This file provides assertions, tests, suites, fixtures,
mocks, stubs and a lengthy description thereof.

Tests

A simple test may look like:

    (deftest test-foo
      (assert (= (+ 2 2) 4))
      (assert (= (* 3 3) 9))
      (assert (= (% 4 2) 0)))

This checks that 2 + 2 = 4, that 3 * 3 = 9, and that 4 % 2 = 0.
(If it isn't, something is seriously wrong!) To run the test:

    (test-foo)    ; `M-x test-foo' interactively

To run the test when it's defined, specify `:run t':

    (deftest test-foo
      :run t
      (assert (= (+ 2 2) 4))
      (assert (= (* 3 3) 9))
      (assert (= (% 4 2) 0)))

Note that it's a good idea to name tests with a prefix like "test-"
to avoid overwriting other functions.

Let's simplify it a bit. Most `assert-' forms defined here accept
multiple (sets of) arguments, similarly to, e.g., `setq':

    (deftest test-foo
      :run t
      (assert
        (= (+ 2 2) 4)
        (= (* 3 3) 9)
        (= (% 4 2) 0)))

This is Lisp, after all! To remove the `=' noise, use `assert-=':

    (deftest test-foo
      :run t
      (assert-=
        (+ 2 2) 4
        (* 3 3) 9
        (% 4 2) 0))

Note that xUnit frameworks sometimes use the reverse order, e.g.,
"assertEquals(4, 2 + 2);", where the expected value comes first.
Here, however, the expectation always comes last, mirroring the
original code.

At this point it's advisable to add some commentary. Tests as well
as assertions can have textual annotations:

    (deftest test-foo
      "Example test."
      :run t
      (assert-=
        "Elementary truths."
        (+ 2 2) 4
        (* 3 3) 9
        (% 4 2) 0))

If the test fails, the annotations show up in the failure report.
(Note, again, that the forms must be ordered as shown above for
the report to make sense.)

For a more BDD-like style, one can write `should' in place
of `assert':

    (deftest test-baz
      "A list."
      (let ((list '(a b c)))
        (push 'd list)
        (should (eq (first list) 'd))))

Related actions may be grouped together with `expect', which only
checks the last form:

    (deftest test-baz
      "A list."
      (let ((list '(a b c)))
        (expect
          "Push elements to the front."
          (push 'd list)
          (eq (first list) 'd))))

This is useful for providing context in the failure report, as well
as in the test itself. `should' statements can also be grouped:

    (deftest test-baz
      "A list."
      (let ((list '(a b c)))
        (expect
          "Push elements to the front."
          (push 'd list)
          (should (eq (first list) 'd))
          (push 'e list)
          (should (eq (first list) 'e)))))

BDD aliases are defined for all assertions: `should-eq' instead of
`assert-eq', `should-=' instead of `assert-=', and so on.
`should' is sufficient in most cases, though: it displays a
recursive inspection of any failing form, and it checks each and
every form it contains.

NOTE: `assert' only accepts multiple arguments inside `deftest'.
Outside `deftest' it's a different macro (defined by cl.el).

Test suites

Tests can be grouped into suites with `defsuite'. The most
straightforward way is simply to wrap it around them:

    (defsuite test-foo-suite
      (deftest test-foo
        (assert-=
          (+ 2 2) 4))
      (deftest test-bar
        (assert-=
          (* 3 3) 9)))

Like tests, the suite is executed with (test-foo-suite),
`M-x test-foo-suite' or `:run t' in the definition. Suites
can also have annotations:

    (defsuite test-foo-suite
      "Example suite."
      :run t
      (deftest test-foo
       (assert-=
         (+ 2 2) 4)))

One can also define the test suite first and then add tests
and suites to it, using the `:suite' keyword or `add-to-suite':

    (defsuite test-foo-suite
      "Example suite.")

    (deftest test-foo
      :suite test-foo-suite
      (assert-=
        (+ 2 2) 4))

    (deftest test-bar
      (assert-=
        (* 3 3) 9))

    (add-to-suite 'test-foo-suite 'test-bar)

Furthermore, `defsuite' forms may nested. (Self-referencing suite
definitions should be avoided, although some safeguards exist to
prevent infinite loops.)

Fixtures

Sometimes it's useful to set up and tear down an environment for
each test in a suite. This can be done with the :setup and
:teardown keyword arguments, which accept a form to evaluate before
and after each test. (You can use `progn' to group expressions.)

    (defsuite test-foo-suite
      "Example suite."
      :setup (wibble)
      :teardown (progn (wobble) (flob))
      (deftest test-foo
        ...)
      (deftest test-bar
        ...))

However, this might not be sufficient: what if the setup and
teardown need to share variables, or the test should be wrapped in
a macro like `save-restriction'? To that end, the more powerful
:fixture keyword argument may be used. It accepts a one-argument
function which is used to call the test:

    (defsuite test-foo-suite
      "Example suite."
      :fixture (lambda (body)
                 (unwind-protect
                     ;; set up environment
                     (save-restriction
                       (wibble)
                       (wobble)
                       ;; run test
                       (funcall body))
                   ;; tear down environment
                   (wubble)
                   (flob)))
      (deftest test-foo
        ...)
      (deftest test-bar
        ...))

As shown above, the function must contain (funcall body) somewhere
in its definition for the test to be run at all. It is good style
to use `unwind-protect' to ensure that the fixture always completes
properly, regardless of the test's outcome.

Finally, there's the :wrap keyword argument, which specifies an
around-advice for the whole test, e.g., :wrap ((wobble) ad-do-it).
See the docstring of `defadvice' for more details on advice. While
the other fixtures are repeated for each test in the suite, :wrap
is executed once for the whole suite. The order is:

                 +-------------------+
                 |:wrap              |
                 |  +==============+ |
                 |  |:setup        | |
                 |  |  +---------+ | |
                 |  |  |:fixture | | |
                 |  |  |  +----+ | | |
                 |  |  |  |TEST| | | |
                 |  |  |  +----+ | | |
                 |  |  +---------+ | |
                 |  |:teardown     | |
                 |  +==============+ |
                 +-------------------+

A test defined as part of a suite carries with it the suite's
fixtures even when called outside the suite. However, when the test
is called by a different suite, that suite's fixtures temporarily
override the fixtures inherited from the original suite.

Any single test may also specify its own fixtures. In that case,
the suite fixtures are wrapped around the test fixtures. However,
no fixtures are executed at all if the test is called from within
another test: the calling test is assumed to provide the necessary
environment.

When defining a function to use as a fixture, make sure to define
it before the tests are run (before the test if using `:run t').

Mocks and stubs

Mocks and stubs are temporary stand-ins for other pieces of code.
They are useful for disabling (or "stubbing out") external behavior
while testing a unit.

To stub a function, use `stub':

    (deftest test-foo
      "Example test."
      (stub foo)
      (assert-not (foo)))  ; foo returns nil

In the rest of the test, any calls to the stubbed function will
return nil. To return a different value, specify the stub's body,
e.g., (stub foo t).

A stub only changes a function's output, not its input: the
argument list remains the same. The stub's body may refer to the
original arguments. To change a function's input too, use `mock':

    (deftest test-foo
      "Example test."
      (mock foo (arg)
        (1+ arg))
      (assert-= (foo 1) 2))  ; foo returns 2

`mock' specifies a temporary function to assign to `foo' for the
duration of the test. Here it increments its argument by one. When
the test completes (or fails), all stubs and mocks are released.

If the same mock is frequently reused, put it in a fixture or
define a function for it and call that function in the test. Just
ensure that it is never called outside a test, otherwise it will
not be released (unless wrapped in `with-mocks-and-stubs').

Dependencies

Reverse dependencies