test
import test
A testing framework: suites, assertions, mocks, snapshots, lifecycle hooks, and reports worth reading.
import test { * }
describe('Calculator', @{
it('adds', @{
expect(2 + 2).to_be(4)
})
it('divides', @{
expect(10 / 4).to_be_close_to(2.5)
})
})
Run it like any other script:
$ zuri run tests/calculator.zu
Calculator
✓ adds
✓ divides
PASS
2 passed • 2 total
suites 1 time 1ms
Importing
import test { * } is the form to use in a test file. It brings
describe, it, expect, mock, the hooks and run into scope,
which is what makes a test read as a test rather than as a series of
calls into a module.
import test is there for anything that is not a test file, and
qualifies the same names: test.conduct('tests'), test.run(). The two
can be combined when a test file wants both.
Declaring
describe(name, body) groups tests and may be nested as deeply as the
subject calls for. it(name, body) declares one. Neither runs anything:
they build a tree, and the tree is run once the file has finished
declaring everything in it. That is what makes focusing, filtering and
counting possible at all.
The run happens on its own, through os.at_exit(). Call run()
explicitly when you want to pass options or read the Summary back;
doing so takes over, and nothing runs a second time.
Focusing and skipping
describe_only('the one I am working on', @{ ... })
describe_skip('not now', @{ ... })
it_only('just this', @{ ... })
it_skip('known broken', @{ ... })
it_todo('handle an empty payload')
As soon as anything is marked only, everything not marked only and
not inside something marked only is skipped. skip always wins over
only.
it('name') with no body is a todo as well: it is reported as one and
counts towards nothing.
Table-driven tests
it_each([
[1, 1, 2],
[2, 3, 5],
[10, -4, 6],
], 'adds $0 and $1 to make $2', @(left, right, total) {
expect(left + right).to_be(total)
})
Each row becomes one test. $0, $1 and so on in the name are replaced
with that row’s values, and $# with the row number. describe_each()
does the same for whole suites.
Per-test options
it('reaches the network', @{ ... }, { retries: 2, tags: ['slow'] })
it('reproduces issue 412', @{ ... }, { failing: true })
it('stays fast', @{ ... }, { timeout: 50 })
| Option | What it does |
|---|---|
retries | run the body again on failure, up to this many times; passing after a retry is reported as flaky |
failing | the test passes when the body fails, and fails when it passes |
timeout | fail the test if it took longer than this many milliseconds |
tags | a list of labels to filter on |
Hooks
describe('Database', @{
before_all(@{ db.connect() })
after_all(@{ db.close() })
before_each(@{ db.begin() })
after_each(@{ db.rollback() })
it('inserts a row', @{ ... })
})
before_all runs once, immediately before the first test in its suite
that is actually going to run, and not at all for a suite everything was
filtered out of. after_all runs after the last one, and runs even when
before_all failed, so it must cope with a setup that got only part of
the way. before_each runs outermost first and after_each innermost
first, and after_each runs even when the test failed.
Asserting
expect(value) gives an object carrying every matcher, each of which
negates through .not and returns itself so several read as one:
expect(port).to_be_int().to_be_between(1024, 65535)
expect(items).not.to_be_empty()
The full list is in test.expect. In short:
- equality:
to_be,to_equal,to_be_same_as,to_be_close_to,to_be_within,to_match_object - truthiness:
to_be_true,to_be_false,to_be_truthy,to_be_falsy,to_be_nil,to_be_defined - types:
to_be_a,to_be_string,to_be_number,to_be_int,to_be_float,to_be_bigint,to_be_bool,to_be_list,to_be_dict,to_be_bytes,to_be_function,to_be_callable,to_be_iterable,to_be_class,to_be_instance,to_be_instance_of - numbers:
to_be_greater_than,to_be_greater_than_or_equal,to_be_less_than,to_be_less_than_or_equal,to_be_between,to_be_positive,to_be_negative,to_be_zero,to_be_divisible_by,to_be_even,to_be_odd,to_be_nan,to_be_finite,to_be_infinite - text:
to_contain,to_contain_ignoring_case,to_equal_ignoring_case,to_start_with,to_end_with,to_match,to_be_blank - collections:
to_have_length,to_be_empty,to_contain_equal,to_contain_all,to_contain_any,to_contain_none,to_contain_exactly,to_have_key,to_have_keys,to_have_value,to_have_property,to_be_sorted,to_have_unique_items - errors:
to_raise,to_raise_instance_of,to_raise_with_message,to_not_raise - mocks:
to_have_been_called,to_have_been_called_times,to_have_been_called_with,to_have_been_last_called_with,to_have_been_nth_called_with,to_have_returned,to_have_returned_with,to_have_raised - output:
to_print,to_print_exactly,to_print_nothing - snapshots:
to_match_snapshot - anything else:
to_satisfy
fail(message) fails outright, for the branch that should never be
reached. assertions(n) and has_assertions() fail a test that did not
assert what it said it would, which is how a test proves that the
callback it put its assertions in actually ran.
Mocking
var send = mock()
notify(send.fn)
expect(send).to_have_been_called_times(1)
expect(send).to_have_been_called_with('hello')
spy_on(object, 'key') replaces a function in place and restores it
after the test, whether the test passed or not. See
test.mock for what can and cannot be spied on.
Snapshots
expect(render(invoice)).to_match_snapshot()
The first run records the value beside the test file and passes; later
runs compare against it. See test.snapshot for the
file format, how to update them, and why a brand new snapshot fails on
CI.
Running
run() takes an options dictionary:
| Option | Default | What it does |
|---|---|---|
reporter | 'spec' | spec, dot, tap, junit, json, ndjson, silent, or a Reporter of your own |
filter | nil | run only tests whose full name contains this, or matches it when it is a regular expression |
tags | nil | run only tests carrying one of these tags |
exclude_tags | nil | skip tests carrying one of these tags |
bail | 0 | stop after this many failures; 0 never stops |
shuffle | false | run in a random order, to catch tests that depend on each other |
seed | nil | the seed to shuffle with; one is chosen and reported when not given |
slow | 300 | milliseconds past which a test’s time is highlighted |
timeout | 0 | a budget applied to every test; 0 means none |
retries | 0 | retries applied to every test |
capture | true | collect each test’s output and show it only when it fails |
verbose | false | show a passing test’s output as well |
update_snapshots | ZURI_UPDATE_SNAPSHOTS | rewrite snapshots instead of checking them |
ci | CI | treat a brand new snapshot as a failure |
exit | false | exit the process with the run’s status when it finishes |
filter and reporter can also come from ZURI_TEST_FILTER and
ZURI_TEST_REPORTER, which is what lets one command be pointed at one
test without editing the file.
run() returns a Summary; summary.exit_code() is 0 when
everything passed. A file that never calls run() gets the same status:
the automatic run sets it through os.set_exit_code() when anything
failed, so CI needs nothing added.
More than one file
conduct(directory) finds every test file under a directory, runs each
in a process of its own, and reports them together. A file that hangs or
crashes takes nothing else down with it. See
test.conduct.
zuri test is that call behind a command line, and is all a project
needs to run its tests directory:
$ zuri test
$ zuri test pricing # just tests/pricing.zu
Call conduct() yourself when the run wants to be under the project’s
own control:
# tests/index.zu
import os
import test
test.conduct(os.dir_name(__file__))
The test API
Every public name in test, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
test.AssertionError | class | A matcher that did not hold. |
test.Case | class | One declared test, and what became of it. |
test.Expect | class | The subject of an assertion, and every matcher that can be applied to it. |
test.Failure | class | One thing that went wrong. |
test.Mock | class | A recording stand-in for a function. |
test.Reporter | class | The interface the runner talks to, with every method doing nothing. |
test.Suite | class | A describe() block: its tests, its nested suites, and its hooks. |
test.Summary | class | What a whole run came to. |
test.TestSetupError | class | The framework was asked to do something that does not make sense: a matcher given the wrong kind of argument,… |
test.after_all | function | Runs once after the last test in the enclosing suite, and only if before_all ran. |
test.after_each | function | Runs after every test in the enclosing suite and everything nested inside it, innermost suite first, whether… |
test.assertions | function | Declares that this test makes exactly count assertions, and fails it if the number turns out to be… |
test.before_all | function | Runs once before the first test in the enclosing suite that is going to run. |
test.before_each | function | Runs before every test in the enclosing suite and everything nested inside it, outermost suite first. |
test.capture_output | function | Runs body and returns everything it printed to standard output. |
test.conduct | function | Discovers test files under directory and runs each in a process of its own. |
test.conduct.CONDUCTED | constant | |
test.conduct.FileResult | class | What became of one file. |
test.conduct.conduct | function | Runs every test file under directory, each in its own process, and reports them together. |
test.conduct.discover | function | Every test file under directory, in the order they will run. |
test.conduct.is_conducted | function | Whether this process was started by the conductor. |
test.context.Frame | class | The test the runner currently has open, as far as anything outside the runner needs to know about it. |
test.context.active | function | Whether a test is running right now. |
test.context.assertion_count | function | How many matchers have run in the current test. |
test.context.begin | function | Opens a frame for a test about to run. |
test.context.check | function | Checks a finished test’s assertion promises against what it actually did. |
test.context.current | function | The frame for the test currently running, or nil outside one. |
test.context.end | function | Closes the current frame and returns it, so the runner can read the final assertion count off it. |
test.context.expect_assertions | function | Declares that the current test makes exactly count assertions. |
test.context.expect_some_assertions | function | Declares that the current test makes at least one assertion. |
test.context.record_assertion | function | Counts one matcher against the current test. |
test.context.snapshot_key | function | Claims the next snapshot key for the current test. |
test.declared | function | The tests declared so far, as a tree, without running any of them. |
test.describe | function | Groups tests, and may be nested. |
test.describe_each | function | Declares one suite per row of a table. |
test.describe_only | function | Runs only this suite, and anything else marked only. |
test.describe_skip | function | Skips this suite. |
test.diff.MAX_DIFF_DEPTH | constant | |
test.diff.MAX_DIFF_LINES | constant | |
test.diff.equal | function | Whether left and right are structurally equal. |
test.diff.render | function | The lines to print underneath a failure message, showing what the two values disagree about. |
test.diff.subset | function | Whether every key in subset is present in value with a structurally equal value, ignoring any key value… |
test.expect | function | Starts an assertion about value. |
test.fail | function | Fails the current test outright. |
test.format.DEFAULT_MAX_DEPTH | constant | |
test.format.DEFAULT_MAX_ITEMS | constant | |
test.format.DEFAULT_MAX_STRING | constant | |
test.format.block | function | value rendered over as many lines as it takes, with nothing abbreviated away. |
test.format.class_name | function | The name of the class value was built from, or nil when it is not an instance. |
test.format.duration | function | A duration in milliseconds, rendered the way a reader wants to see it: whole milliseconds below a second, and… |
test.format.inline | function | value rendered as a single line, abbreviated to stay readable inside a sentence. |
test.format.inline_plain | function | inline() with colour suppressed, whatever the terminal supports. |
test.format.plural | function | count followed by singular or plural, whichever the count calls for. |
test.format.properties_of | function | The names of an instance’s own properties, in declaration order. |
test.format.property_of | function | Reads one of an instance’s own properties by name. |
test.format.serialize | function | block() with colour suppressed and dictionary keys sorted, which together make the output stable enough to… |
test.format.type_label | function | How to name value’s type in a sentence a person reads. |
test.format.type_name | function | The name of value’s type: 'nil', 'bool', 'int', 'float', 'bigint', 'string', 'bytes',… |
test.format.with_article | function | word with the article that belongs in front of it. |
test.has_assertions | function | Declares that this test makes at least one assertion, and fails it if none did. |
test.it | function | Declares one test. |
test.it_each | function | Declares one test per row of a table. |
test.it_only | function | Runs only this test, and anything else marked only. |
test.it_skip | function | Skips this test, while still listing it in the report. |
test.it_todo | function | Notes a test that has not been written yet. |
test.mock | function | A new recording function. |
test.mock.Call | class | One recorded call. |
test.mock.mock_of | function | The Mock behind value, which may be a Mock already or the plain function one handed out. |
test.mock.reset_all | function | Forgets the calls recorded by every live mock, leaving the mocks themselves and their behaviours in place. |
test.mock.restore_all | function | Undoes every spy and forgets every mock built so far. |
test.reporter.Dot | class | One character per test, wrapped to the terminal, then the same failure detail and summary the spec reporter… |
test.reporter.Json | class | One JSON document at the end, holding the summary and every test. |
test.reporter.Junit | class | JUnit XML, which is the format nearly every CI system knows how to turn into a test report page. |
test.reporter.Ndjson | class | One JSON object per line, emitted as each thing happens. |
test.reporter.PREFIX | constant | What marks a line of ndjson output as protocol rather than as something the test file happened to print. |
test.reporter.Silent | class | No output at all, for a caller reading the returned Summary instead. |
test.reporter.Spec | class | The default: the suite tree, one line per test, and every failure written out in full at the end. |
test.reporter.Tap | class | TAP version 14: one ok/not ok line per test, with failure detail in a YAML block underneath. |
test.reporter.create | function | Builds a reporter by name. |
test.reset | function | Forgets every declaration and every loaded snapshot file. |
test.result.FAILED | constant | It ran and something did not hold. |
test.result.FLAKY | constant | It failed, was retried, and then passed. |
test.result.PASSED | constant | It ran and every assertion held. |
test.result.PENDING | constant | A test was declared and not yet run. |
test.result.SKIPPED | constant | It was not run: skip, a filter, or another test’s only. |
test.result.TODO | constant | It was declared with no body, as a note to write it later. |
test.run | function | Runs everything declared so far, and reports it. |
test.runner.after_all | function | |
test.runner.after_each | function | |
test.runner.before_all | function | |
test.runner.before_each | function | |
test.runner.describe | function | Opens a suite, runs body to collect what is inside it, and closes it again. |
test.runner.it | function | Declares one test. |
test.runner.reset | function | Throws away every declaration and every snapshot store, so a second run in the same process starts from… |
test.runner.root | function | The tree as it stands, for a caller that wants to look at what was declared without running it. |
test.runner.run | function | Runs everything declared so far and reports it. |
test.snapshot.BODY_INDENT | constant | |
test.snapshot.HEADER | constant | |
test.snapshot.Store | class | Every snapshot recorded for one test file, and the file they live in. |
test.snapshot.check | function | Compares one value against what the snapshot file holds for key, writing it down when there is nothing… |
test.snapshot.flush | function | Writes every changed store back to disk, and in update mode drops entries the run never asked about. |
test.snapshot.path_for | function | The .snap file that belongs to a given test file. |
test.snapshot.recorded_lines | function | The lines of the snapshot at key, as a list, for a reporter that wants to show what was recorded. |
test.snapshot.reset | function | Forgets every loaded store and zeroes the counters, so a second run in the same process starts clean. |
test.snapshot.set_strict | function | Turns the strict, new-snapshots-fail behaviour on or off. |
test.snapshot.set_updating | function | Turns snapshot rewriting on or off. |
test.snapshot.store_for | function | The store for a test file, read from disk the first time it is asked for and kept afterwards. |
test.snapshot.strict | function | Whether a brand new snapshot counts as a failure, which is what it should be anywhere nobody is going to look… |
test.snapshot.summary | function | What happened to snapshots over the whole run. |
test.snapshot.updating | function | Whether snapshots are being rewritten rather than checked. |
test.source.Frame | class | One frame of a stack trace, pulled apart. |
test.source.clear_cache | function | Forgets every file read for a code frame. |
test.source.code_frame | function | The source around a failure, with the offending line marked. |
test.source.origin | function | The innermost frame of stacktrace outside the test module: where the failing line actually is. |
test.source.parse_frame | function | Splits one stack trace line into a Frame. |
test.source.short_path | function | A path written relative to the working directory when it is under it, and left alone when it is not. |
test.source.user_frames | function | Every frame of stacktrace that belongs to the code under test, innermost first. |
test.spy_on | function | Replaces target[key] with a recording stand-in that calls through to the original, and returns the Mock… |
test.style.badge | function | A filled, inverted label, the way a status banner reads in a CI log. |
test.style.blue | function | Todo entries and other informational notes. |
test.style.bold | function | Emphasis. |
test.style.cyan | function | Structure: suite names, headings. |
test.style.dim | function | De-emphasis, for detail that should recede: timings, counts, the suite path above a failure. |
test.style.enabled | function | Whether colour is currently being emitted. |
test.style.green | function | Success. |
test.style.grey | function | Punctuation and separators. |
test.style.indent | function | Two spaces per level, the indent every nested suite and test line in the report is built from. |
test.style.inverse | function | Reserved for text that has to be found instantly on a busy screen. |
test.style.magenta | function | Values in a rendered diff or failure message. |
test.style.pad_left | function | Pads text on the left to width visible columns, leaving it alone when it is already that wide or wider. |
test.style.pad_right | function | Pads text on the right to width visible columns, leaving it alone when it is already that wide or wider. |
test.style.red | function | Failure. |
test.style.rule | function | count copies of character. |
test.style.set_enabled | function | Forces colour on or off, overriding what the environment said. |
test.style.set_unicode | function | Forces the Unicode symbol set on or off. |
test.style.strip | function | text with every ANSI escape sequence removed. |
test.style.symbols | function | The symbol set in use, keyed by role. |
test.style.truncate | function | Shortens text to at most width visible columns, marking the cut with an ellipsis. |
test.style.unicode | function | Whether the Unicode symbol set is in use, as opposed to the ASCII fallbacks. |
test.style.visible_length | function | How many columns text occupies once its escape sequences are discounted. |
test.style.white | function | Plain foreground, used to lift a key out of dimmed surroundings. |
test.style.width | function | The width to lay the report out to. |
test.style.yellow | function | Skipped and other deliberate non-results. |
test.use_color | function | Forces colour in the report on or off, overriding what the environment said. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
test.conduct | import test.conduct | Running a directory of test files, each in a process of its own. |
test.context | import test.context | What is true right now, while one particular test is running. |
test.diff | import test.diff | Structural equality, and the rendering of what two unequal values disagree about. |
test.error | test.error.* | The errors the test module raises, kept in a module of their own so that the assertion side and the runner… |
test.expect | test.expect.* | expect(value) and everything that can be said about a value once you have one. |
test.format | import test.format | Turning any Zuri value into text a person can read in a failure message. |
test.mock | test.mock.* | Test doubles: functions that record how they were called, and stand-ins that replace a real one for the… |
test.reporter | test.reporter.* | How a run is shown. |
test.result | test.result.* | The shapes a test run is made of: the tree the declarations build, and what running it produced. |
test.runner | import test.runner | Collecting the tests a file declares, deciding which of them to run, and running them. |
test.snapshot | import test.snapshot | Snapshot testing: recording what a value looked like the first time and failing when it stops looking like… |
test.source | import test.source | Working out where a failure came from, and showing the code that was there. |
test.style | import test.style | Terminal presentation for the test module: colour, symbols, width, and the padding helpers that keep a… |
Functions
describe()
test.describe(name: string, body: function) -> Suite
Groups tests, and may be nested.
describe('Stack', @{
describe('push()', @{
it('grows the stack', @{ ... })
})
})
Parameters
name(string)body(function)
Returns Suite
describe_only()
test.describe_only(name: string, body: function) -> Suite
Runs only this suite, and anything else marked only.
Parameters
name(string)body(function)
Returns Suite
describe_skip()
test.describe_skip(name: string, body: function) -> Suite
Skips this suite. Its tests are still declared, still counted, and still listed; they simply do not run.
Parameters
name(string)body(function)
Returns Suite
it()
test.it(name: string, body: ?function, options: ?dict) -> Case
Declares one test.
it('returns the total', @{
expect(total(cart)).to_be(42)
})
Parameters
name(string)body(?function) — left out, this is a todo.options(?dict) —retries,failing,timeout,tags. See the module documentation for what each does.
Returns Case
it_only()
test.it_only(name: string, body: function, options: ?dict) -> Case
Runs only this test, and anything else marked only.
Parameters
name(string)body(function)options(?dict)
Returns Case
it_skip()
test.it_skip(name: string, body: ?function, options: ?dict) -> Case
Skips this test, while still listing it in the report.
Parameters
name(string)body(?function)options(?dict)
Returns Case
it_todo()
test.it_todo(name: string) -> Case
Notes a test that has not been written yet.
It is listed in the report and counts towards nothing, which is the point: a todo is a reminder that survives being committed.
Parameters
name(string)
Returns Case
it_each()
test.it_each(table: list, name: string, body: function, options: ?dict)
Declares one test per row of a table.
it_each([
['', 0],
['a', 1],
['hello', 5],
], 'says $0 is $1 characters long', @(text, length) {
expect(text.length()).to_be(length)
})
Each row is spread across body’s parameters, so a three-column table
calls a three-parameter function. A row that is not a list counts as a
single-column row.
Parameters
table(list) — the rows.name(string) —$0,$1, … are replaced with that row’s values, and$#with the row number, counting from one.body(function)options(?dict) — applied to every test the table declares.
Returns — list: the declared tests.
describe_each()
test.describe_each(table: list, name: string, body: function)
Declares one suite per row of a table.
The row is spread across body’s parameters exactly as it_each()
does, so the whole suite is written once against whatever the row holds.
describe_each([
['json', json_codec],
['yaml', yaml_codec],
], '$0 codec', @(name, codec) {
it('round-trips a dictionary', @{
expect(codec.decode(codec.encode({ a: 1 }))).to_equal({ a: 1 })
})
})
Parameters
table(list)name(string)body(function)
Returns — list: the declared suites.
before_all()
test.before_all(body: function)
Runs once before the first test in the enclosing suite that is going to run.
Parameters
body(function)
after_all()
test.after_all(body: function)
Runs once after the last test in the enclosing suite, and only if
before_all ran. It runs when before_all raised as well, since a
setup that failed part way may already hold what needs releasing, so it
must cope with whatever the setup got as far as.
Parameters
body(function)
before_each()
test.before_each(body: function)
Runs before every test in the enclosing suite and everything nested inside it, outermost suite first.
Parameters
body(function)
after_each()
test.after_each(body: function)
Runs after every test in the enclosing suite and everything nested inside it, innermost suite first, whether the test passed or not.
Parameters
body(function)
run()
test.run(options: ?dict) -> Summary
Runs everything declared so far, and reports it.
Calling this is optional. A file that declares tests and never says anything else runs them as it ends, and fails the process when they fail. Call it when you want to pass options or read the result:
run({ reporter: 'dot', bail: 1 })
var summary = run({ filter: 'parser' })
echo summary.passed
Parameters
options(?dict) — see the module documentation for every option and its default.
Returns Summary
Note: calling it takes over from the automatic run, so nothing is reported twice. Calling it twice does run everything twice.
conduct()
test.conduct(directory: ?string, options: ?dict)
Discovers test files under directory and runs each in a process of its
own.
Parameters
directory(?string) —'tests'by default.options(?dict) — see {test.conduct}.
Returns — dict: the combined result.
declared()
test.declared() -> Suite
The tests declared so far, as a tree, without running any of them.
Returns Suite
reset()
test.reset()
Forgets every declaration and every loaded snapshot file.
For a process that runs more than one suite, and for the tests of this framework itself.
use_color()
test.use_color(on: bool)
Forces colour in the report on or off, overriding what the environment said.
Parameters
on(bool)
2021, Richard Ore and Zuri contributors