Testworks Usage#

Testworks is a Dylan unit testing library.

See also: Testworks Reference

Quick Start#

For the impatient, this section summarizes most of what you need to know to use Testworks.

Add use testworks; to both your test library and test module.

Tests contain arbitrary code and at least one assertion:

define test test-fn1 ()
  let v = do-something();
  assert-equal("expected-value", fn1(v));
  assert-signals(<error>, fn1(v, key: 7), "regression test for bug 12345");
end;

If there are no assertions in a test it is considered “not implemented”, which is displayed in the output (as a reminder to implement it) but is not considered a failure.

See also: Assertions for other assertion macros.

Benchmarks do not require any assertions and are automatically given the “benchmark” tag:

// Benchmark fn1
define benchmark fn1-benchmark ()
  fn1()
end;

See also, benchmark-repeat.

If you have a large or complex test library, “suites” may be used to organize tests into groups (for example one suite per module) and may be nested arbitrarily.

define suite my-library-suite ()
  suite module1-suite;
  suite module2-suite;
  test some-other-test;
end;

Note

Suites must be defined textually after the other suites and tests they contain.

To run your tests of course you need an executable and there are two ways to accomplish this:

  1. Have your library call run-test-application and compile it as an executable. With no arguments run-test-application runs all tests and benchmarks, as filtered by the Testworks command-line options.

    If you prefer to manually organize your tests with suites, pass your top-level suite to run-test-application and that determines the initial set of tests that are filtered by the command line.

    Note

    If you forget to add a test to any suite, the test will not be run.

  2. Compile your test library as a shared library and run it with the testworks-run application. For example, for the foo-test library:

    _build/bin/testworks-run --load libfoo-test.so
    

In both cases run-test-application parses the command line so the options are the same. Use --help to see all options.

See Suites for a way to organize large test suites.

Defining Tests#

Assertions#

An assertion accepts an expression to evaluate and report back on, saying if the expression passed, failed, or crashed (i.e., signaled an error). As an example, in

assert-true(foo > bar)

the expression foo > bar is compared to #f, and the result is recorded by the test harness. Failing (or crashing) cause the test to terminate and skip the remaining assertions in that test.

Note

You may also find check-* macros in existing test suites. These are deprecated assertions that do not cause the test to terminate and require a description of the assertion as the first argument.

See the Testworks Reference for detailed documentation on the available assertion macros:

Each of these takes an optional description, after the required arguments, which will be displayed if the assertion fails. If the description isn’t provided, Testworks makes one from the expressions passed to the assertion macro. For example, assert-true(2 > 3) produces this failure message:

(2 > 3) is true failed [expression "(2 > 3)" evaluates to #f]

In general, Testworks should be pretty good at reporting the actual values that caused the failure so it shouldn’t be necessary to include them in the description all the time. Usually if your test iterates over various inputs it’s a good idea to provide a description so the failing input can be easily identified.

If you do provide a description it may either be a single value to display, as with format-to-string("%s", v), or a format string and corresponding format arguments. These are all valid:

assert-equal(a, b);     // auto-generated description
assert-equal(a, b, a);  // a used as description
assert-equal(a, b, "does %= = %=?", a, b);  // formatted description

Tests#

Tests contain assertions and arbitrary code needed to support those assertions. Each test may be part of a suite. Use the test-definer macro to define a test:

define test NAME (#key EXPECTED-FAILURE?, TAGS)
  BODY
end;

For example:

define test my-test ()
  assert-equal(2, 3);
  assert-equal(#f, #f);
  assert-true(identity(#t), "Check identity function");
end;

The result looks like this:

$ _build/bin/my-test-suite
Running suite my-test-suite:
Running test my-test:
  2 = 3: [2 and 3 are not =.]
   FAILED in 0.000257s and 17KiB

my-test failed
  #f = #f passed
  2 = 3 failed [2 and 3 are not =.]
Ran 2 checks: FAILED (1 failed)
Ran 1 test: FAILED (1 failed)
FAILED in 0.000257 seconds

Note that the third assertion was not executed since the second one failed and terminated my-test.

Tests may be tagged with arbitrary strings, providing a way to select or filter out tests to run:

define test my-test-2 (tags: #["huge"])
  ...huge test that takes a long time...
end test;

define test my-test-3 (tags: #["huge", "verbose"])
  ...test with lots of output...
end test;

Tags can then be passed on the Testworks command-line. For example, this skips both of the above tests:

$ _build/bin/my-test-suite-app --tag=-huge --tag=-verbose

Negative tags take precedence, so --tag=huge --tag=-verbose runs my-test-2 and skips my-test-3.

If the test is expected to fail, or fails under some conditions, Testworks can be made aware of this:

define test failing-test
    (expected-to-fail-reason: "bug 1234")
  assert-true(#f);
end test;

define test fails-on-windows
    (expected-to-fail?: method () $os-name = #"win32" end,
     expected-to-fail-reason: "blah is not implemented for WIN32 platform")
  if ($os-name = #"win32")
    assert-false(#t);
  else
    assert-true(#t);
  end if;
end test;

A test that is expected to fail and then fails is considered to be a passing test. If the test succeeds unexpectedly, it is considered a failing test. When marking a test as expected to fail, expected-to-fail-reason: is required and expected-to-fail?: is optional, and normally unnecessary. An example of a good reason is a bug URL or other bug reference.

Note

When providing a value for expected-to-fail?: always provide a method of no arguments. For example, instead of expected-to-fail?: $os-name == #"win32" use expected-to-fail?: method () $os-name == #"win32" end. The former is equivalent to expected-to-fail?: #f on non-Windows platforms and results in an UNEXPECTED SUCCESS result. This is because the (required) reason string is used as shorthand to indicate that failure is expected even when expected-to-fail?: is #f.

Test setup and teardown is accomplished with normal Dylan code using block () ... cleanup ... end;

define test foo ()
  block ()
    do-setup-stuff();
    assert-equal(...);
    assert-equal(...);
  cleanup
    do-teardown-stuff()
  end
end;

Benchmarks#

Benchmarks are like tests except for:

  • They do not require any assertions. (They pass unless they signal an error.)

  • They are automatically assigned the “benchmark” tag.

The benchmark-definer macro is like test-definer:

define benchmark my-benchmark ()
  ...body...
end;

Benchmarks may be added to suites:

define suite my-benchmarks-suite ()
  benchmark my-benchmark;
end;

Benchmarks and tests may be combined in the same suite, but this is discouraged. It is preferable to have separate libraries for the two since benchmarks often take longer to run and may not necessarily need to be run for every commit.

See also, benchmark-repeat.

Suites#

Suites are an optional feature that may be used to organize your tests into a hierarchy. Suites contain tests, benchmarks, and other suites. A suite is defined with the suite-definer macro. The format is:

define suite NAME (#key setup-function, cleanup-function)
    test TEST-NAME;
    benchmark BENCHMARK-NAME;
    suite SUITE-NAME;
end;

For example:

define suite first-suite ()
  test my-test;
  test example-test;
  test my-test-2;
end;

define suite second-suite ()
  suite first-suite;
  test my-test;
end;

Suites can specify setup and cleanup functions via the keyword arguments setup-function and cleanup-function. These can be used for things like establishing database connections, initializing sockets and so on.

A simple example of doing this can be seen in the http-server test suite:

define suite http-test-suite (setup-function: start-sockets)
  suite http-server-test-suite;
  suite http-client-test-suite;
end;

Interface Specification Suites#

The interface-specification-suite-definer macro creates a normal test suite, much like define suite does, but based on an interface specification. For example,

define interface-specification-suite time-specification-suite ()
  sealed instantiable class <time> (<object>);
  constant $utc :: <zone>;
  variable *zone* :: <zone>;
  sealed generic function in-zone (<time>, <zone>) => (<time>);
  function now (#"key", #"zone") => (<time>);
  ...
end;

The specification usually has one clause, or “spec”, for each name exported from your public interface module. Each spec creates a test named test-{name}-specification to verify that the implementation matches the spec for {name}. For example, by checking that the names are bound, that their bindings have the correct types, that functions accept the right number and types of arguments, etc.

Specification suites are otherwise just normal suites. They may include other arbitrary tests and child suites if desired:

define interface-specification-suite time-suite ()
  ...
  test test-time-still-moving-forward;
  suite time-travel-test-suite;
end;

This also means that if your interface is large you may use multiple interface-specification-suite-definer forms and then group them together.

See interface-specification-suite-definer for more details on the various kinds of specs.

Organizing Tests for One Library#

If you don’t use suites, the only organization you need is to name your tests and benchmarks uniquely, and you can safely skip the rest of this section. If you do use suites, read on….

Tests are used to combine related assertions into a unit, and suites further organize related tests and benchmarks. Suites may also contain other suites.

It is common for the test suite for library xxx to export a single test suite named xxx-test-suite, which is further subdivided into sub-suites, tests, and benchmarks as appropriate for that library. Some suites may be exported so that they can be included as a component suite in combined test suites that cover multiple related libraries. (The alternative to this approach is running each library’s tests as a separate executable.)

Note

It is an error for a test to be included in a suite multiple times, even transitively. Doing so would result in a misleading pass/fail ratio, and it is more likely to be a mistake than to be intentional.

The overall structure of a test library that is intended to be included in a combined test library may look something like this:

// --- library.dylan ---

define library xxx-tests
  use common-dylan;
  use testworks;
  use xxx;                 // the library you are testing
  export xxx-tests;        // so other test libs can include it
end;

define module xxx-tests
  use common-dylan;
  use testworks;
  use xxx;                 // the module you are testing
  export xxx-test-suite;   // so other suites can include it
end;

// --- main.dylan ---

define test my-awesome-test ()
  assert-true(...);
  assert-equal(...);
  ...
end;

define benchmark my-awesome-benchmark ()
  awesomely-slow-function();
end;

define suite xxx-test-suite ()
  test my-awesome-test;
  benchmark my-awesome-benchmark;
  suite my-awesome-other-suite;
  ...
end;

Running Tests As A Stand-alone Application#

If you don’t need to export any suites so they can be included in a higher-level combined test suite library (i.e., if you’re happy running your test suite library as an executable) then you can simply call run-test-application to parse the standard testworks command-line options and run the specified tests:

run-test-application();

and you can skip the rest of this section.

If you need to export a suite for use by another library, then you must also define a separate executable library, traditionally named “xxx-test-suite-app”, which calls run-test-application().

Here’s an example of such an application library:

1. The file library.dylan which must use at least the library that exports the test suite, and testworks:

Module:    dylan-user
Synopsis:  An application library for xxx-test-suite

define library xxx-test-suite-app
  use xxx-test-suite;
  use testworks;
end;

define module xxx-test-suite-app
  use xxx-test-suite;
  use testworks;
end;
  1. The file xxx-test-suite-app.dylan which simply contains a call to the method run-test-application:

Module: xxx-test-suite-app

run-test-application();
  1. The file xxx-test-suite-app.lid which specifies the names of the source files:

Library: xxx-test-suite-app
Target-type: executable
Files: library.dylan
       xxx-test-suite-app.dylan

Once a library has been defined in this fashion it can be compiled into an executable with dylan-compiler -build xxx-test-suite-app.lid and run with xxx-test-suite-app --help.

Reports#

The --report and --report-file options can be used to write a full report of test run results so that those results can be compared with subsequent test runs, for example to find regressions. These are the available report types:

failures

Prints out only the list of failures and a summary.

json

Outputs JSON objects that match the suite/test/assertion tree structure, with full detail.

summary (the default)

Prints out only a summary of how many assertions, tests and suites were executed, passed, failed or crashed.

surefire

Outputs XML in Surefire format. This elides information about specific assertions. This format is supported by various tools such as Jenkins.

xml

Outputs XML that directly matches the suite/test/assertion tree structure, with full detail.

Comparing Test Results#

* To be filled in *

Quick version:

  • (master branch)$ my-test-suite –report json –report-file out1.json

  • (your branch)$ my-test-suite –report json –report-file out2.json

  • $ testworks-report out1.json out2.json