Testing simple functions that have no side effects and no dependencies on their environment is easy. Such tests often look like this:
(ert-deftest ert-test-mismatch () (should (eql (cl-mismatch "" "") nil)) (should (eql (cl-mismatch "" "a") 0)) (should (eql (cl-mismatch "a" "a") nil)) (should (eql (cl-mismatch "ab" "a") 1)) (should (eql (cl-mismatch "Aa" "aA") 0)) (should (eql (cl-mismatch '(a b c) '(a b d)) 2)))
This test calls the function cl-mismatch
several times with
various combinations of arguments and compares the return value to the
expected return value. (Some programmers prefer (should (eql
EXPECTED ACTUAL))
over the (should (eql ACTUAL EXPECTED))
shown here. ERT works either way.)
Here’s a more complicated test:
(ert-deftest ert-test-record-backtrace () (let ((test (make-ert-test :body (lambda () (ert-fail "foo"))))) (let ((result (ert-run-test test))) (should (ert-test-failed-p result)) (with-temp-buffer (ert--print-backtrace (ert-test-failed-backtrace result)) (goto-char (point-min)) (end-of-line) (let ((first-line (buffer-substring-no-properties (point-min) (point)))) (should (equal first-line " signal(ert-test-failed (\"foo\"))")))))))
This test creates a test object using make-ert-test
whose body
will immediately signal failure. It then runs that test and asserts
that it fails. Then, it creates a temporary buffer and invokes
ert--print-backtrace
to print the backtrace of the failed test
to the current buffer. Finally, it extracts the first line from the
buffer and asserts that it matches what we expect. It uses
buffer-substring-no-properties
and equal
to ignore text
properties; for a test that takes properties into account,
buffer-substring
and ert-equal-including-properties
could be used instead.
The reason why this test only checks the first line of the backtrace
is that the remainder of the backtrace is dependent on ERT’s internals
as well as whether the code is running interpreted or compiled. By
looking only at the first line, the test checks a useful property—that
the backtrace correctly captures the call to signal
that
results from the call to ert-fail
—without being brittle.
This example also shows that writing tests is much easier if the code under test was structured with testing in mind.
For example, if ert-run-test
accepted only symbols that name
tests rather than test objects, the test would need a name for the
failing test, which would have to be a temporary symbol generated with
make-symbol
, to avoid side effects on Emacs’s state. Choosing
the right interface for ert-run-tests
allows the test to be
simpler.
Similarly, if ert--print-backtrace
printed the backtrace to a
buffer with a fixed name rather than the current buffer, it would be
much harder for the test to undo the side effect. Of course, some
code somewhere needs to pick the buffer name. But that logic is
independent of the logic that prints backtraces, and keeping them in
separate functions allows us to test them independently.
A lot of code that you will encounter in Emacs was not written with testing in mind. Sometimes, the easiest way to write tests for such code is to restructure the code slightly to provide better interfaces for testing. Usually, this makes the interfaces easier to use as well.