Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 })
OptionWhat it does
retriesrun the body again on failure, up to this many times; passing after a retry is reported as flaky
failingthe test passes when the body fails, and fails when it passes
timeoutfail the test if it took longer than this many milliseconds
tagsa 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:

OptionDefaultWhat it does
reporter'spec'spec, dot, tap, junit, json, ndjson, silent, or a Reporter of your own
filternilrun only tests whose full name contains this, or matches it when it is a regular expression
tagsnilrun only tests carrying one of these tags
exclude_tagsnilskip tests carrying one of these tags
bail0stop after this many failures; 0 never stops
shufflefalserun in a random order, to catch tests that depend on each other
seednilthe seed to shuffle with; one is chosen and reported when not given
slow300milliseconds past which a test’s time is highlighted
timeout0a budget applied to every test; 0 means none
retries0retries applied to every test
capturetruecollect each test’s output and show it only when it fails
verbosefalseshow a passing test’s output as well
update_snapshotsZURI_UPDATE_SNAPSHOTSrewrite snapshots instead of checking them
ciCItreat a brand new snapshot as a failure
exitfalseexit 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.

NameKindSummary
test.AssertionErrorclassA matcher that did not hold.
test.CaseclassOne declared test, and what became of it.
test.ExpectclassThe subject of an assertion, and every matcher that can be applied to it.
test.FailureclassOne thing that went wrong.
test.MockclassA recording stand-in for a function.
test.ReporterclassThe interface the runner talks to, with every method doing nothing.
test.SuiteclassA describe() block: its tests, its nested suites, and its hooks.
test.SummaryclassWhat a whole run came to.
test.TestSetupErrorclassThe framework was asked to do something that does not make sense: a matcher given the wrong kind of argument,…
test.after_allfunctionRuns once after the last test in the enclosing suite, and only if before_all ran.
test.after_eachfunctionRuns after every test in the enclosing suite and everything nested inside it, innermost suite first, whether…
test.assertionsfunctionDeclares that this test makes exactly count assertions, and fails it if the number turns out to be…
test.before_allfunctionRuns once before the first test in the enclosing suite that is going to run.
test.before_eachfunctionRuns before every test in the enclosing suite and everything nested inside it, outermost suite first.
test.capture_outputfunctionRuns body and returns everything it printed to standard output.
test.conductfunctionDiscovers test files under directory and runs each in a process of its own.
test.conduct.CONDUCTEDconstant
test.conduct.FileResultclassWhat became of one file.
test.conduct.conductfunctionRuns every test file under directory, each in its own process, and reports them together.
test.conduct.discoverfunctionEvery test file under directory, in the order they will run.
test.conduct.is_conductedfunctionWhether this process was started by the conductor.
test.context.FrameclassThe test the runner currently has open, as far as anything outside the runner needs to know about it.
test.context.activefunctionWhether a test is running right now.
test.context.assertion_countfunctionHow many matchers have run in the current test.
test.context.beginfunctionOpens a frame for a test about to run.
test.context.checkfunctionChecks a finished test’s assertion promises against what it actually did.
test.context.currentfunctionThe frame for the test currently running, or nil outside one.
test.context.endfunctionCloses the current frame and returns it, so the runner can read the final assertion count off it.
test.context.expect_assertionsfunctionDeclares that the current test makes exactly count assertions.
test.context.expect_some_assertionsfunctionDeclares that the current test makes at least one assertion.
test.context.record_assertionfunctionCounts one matcher against the current test.
test.context.snapshot_keyfunctionClaims the next snapshot key for the current test.
test.declaredfunctionThe tests declared so far, as a tree, without running any of them.
test.describefunctionGroups tests, and may be nested.
test.describe_eachfunctionDeclares one suite per row of a table.
test.describe_onlyfunctionRuns only this suite, and anything else marked only.
test.describe_skipfunctionSkips this suite.
test.diff.MAX_DIFF_DEPTHconstant
test.diff.MAX_DIFF_LINESconstant
test.diff.equalfunctionWhether left and right are structurally equal.
test.diff.renderfunctionThe lines to print underneath a failure message, showing what the two values disagree about.
test.diff.subsetfunctionWhether every key in subset is present in value with a structurally equal value, ignoring any key value…
test.expectfunctionStarts an assertion about value.
test.failfunctionFails the current test outright.
test.format.DEFAULT_MAX_DEPTHconstant
test.format.DEFAULT_MAX_ITEMSconstant
test.format.DEFAULT_MAX_STRINGconstant
test.format.blockfunctionvalue rendered over as many lines as it takes, with nothing abbreviated away.
test.format.class_namefunctionThe name of the class value was built from, or nil when it is not an instance.
test.format.durationfunctionA duration in milliseconds, rendered the way a reader wants to see it: whole milliseconds below a second, and…
test.format.inlinefunctionvalue rendered as a single line, abbreviated to stay readable inside a sentence.
test.format.inline_plainfunctioninline() with colour suppressed, whatever the terminal supports.
test.format.pluralfunctioncount followed by singular or plural, whichever the count calls for.
test.format.properties_offunctionThe names of an instance’s own properties, in declaration order.
test.format.property_offunctionReads one of an instance’s own properties by name.
test.format.serializefunctionblock() with colour suppressed and dictionary keys sorted, which together make the output stable enough to…
test.format.type_labelfunctionHow to name value’s type in a sentence a person reads.
test.format.type_namefunctionThe name of value’s type: 'nil', 'bool', 'int', 'float', 'bigint', 'string', 'bytes',…
test.format.with_articlefunctionword with the article that belongs in front of it.
test.has_assertionsfunctionDeclares that this test makes at least one assertion, and fails it if none did.
test.itfunctionDeclares one test.
test.it_eachfunctionDeclares one test per row of a table.
test.it_onlyfunctionRuns only this test, and anything else marked only.
test.it_skipfunctionSkips this test, while still listing it in the report.
test.it_todofunctionNotes a test that has not been written yet.
test.mockfunctionA new recording function.
test.mock.CallclassOne recorded call.
test.mock.mock_offunctionThe Mock behind value, which may be a Mock already or the plain function one handed out.
test.mock.reset_allfunctionForgets the calls recorded by every live mock, leaving the mocks themselves and their behaviours in place.
test.mock.restore_allfunctionUndoes every spy and forgets every mock built so far.
test.reporter.DotclassOne character per test, wrapped to the terminal, then the same failure detail and summary the spec reporter…
test.reporter.JsonclassOne JSON document at the end, holding the summary and every test.
test.reporter.JunitclassJUnit XML, which is the format nearly every CI system knows how to turn into a test report page.
test.reporter.NdjsonclassOne JSON object per line, emitted as each thing happens.
test.reporter.PREFIXconstantWhat marks a line of ndjson output as protocol rather than as something the test file happened to print.
test.reporter.SilentclassNo output at all, for a caller reading the returned Summary instead.
test.reporter.SpecclassThe default: the suite tree, one line per test, and every failure written out in full at the end.
test.reporter.TapclassTAP version 14: one ok/not ok line per test, with failure detail in a YAML block underneath.
test.reporter.createfunctionBuilds a reporter by name.
test.resetfunctionForgets every declaration and every loaded snapshot file.
test.result.FAILEDconstantIt ran and something did not hold.
test.result.FLAKYconstantIt failed, was retried, and then passed.
test.result.PASSEDconstantIt ran and every assertion held.
test.result.PENDINGconstantA test was declared and not yet run.
test.result.SKIPPEDconstantIt was not run: skip, a filter, or another test’s only.
test.result.TODOconstantIt was declared with no body, as a note to write it later.
test.runfunctionRuns everything declared so far, and reports it.
test.runner.after_allfunction
test.runner.after_eachfunction
test.runner.before_allfunction
test.runner.before_eachfunction
test.runner.describefunctionOpens a suite, runs body to collect what is inside it, and closes it again.
test.runner.itfunctionDeclares one test.
test.runner.resetfunctionThrows away every declaration and every snapshot store, so a second run in the same process starts from…
test.runner.rootfunctionThe tree as it stands, for a caller that wants to look at what was declared without running it.
test.runner.runfunctionRuns everything declared so far and reports it.
test.snapshot.BODY_INDENTconstant
test.snapshot.HEADERconstant
test.snapshot.StoreclassEvery snapshot recorded for one test file, and the file they live in.
test.snapshot.checkfunctionCompares one value against what the snapshot file holds for key, writing it down when there is nothing…
test.snapshot.flushfunctionWrites every changed store back to disk, and in update mode drops entries the run never asked about.
test.snapshot.path_forfunctionThe .snap file that belongs to a given test file.
test.snapshot.recorded_linesfunctionThe lines of the snapshot at key, as a list, for a reporter that wants to show what was recorded.
test.snapshot.resetfunctionForgets every loaded store and zeroes the counters, so a second run in the same process starts clean.
test.snapshot.set_strictfunctionTurns the strict, new-snapshots-fail behaviour on or off.
test.snapshot.set_updatingfunctionTurns snapshot rewriting on or off.
test.snapshot.store_forfunctionThe store for a test file, read from disk the first time it is asked for and kept afterwards.
test.snapshot.strictfunctionWhether a brand new snapshot counts as a failure, which is what it should be anywhere nobody is going to look…
test.snapshot.summaryfunctionWhat happened to snapshots over the whole run.
test.snapshot.updatingfunctionWhether snapshots are being rewritten rather than checked.
test.source.FrameclassOne frame of a stack trace, pulled apart.
test.source.clear_cachefunctionForgets every file read for a code frame.
test.source.code_framefunctionThe source around a failure, with the offending line marked.
test.source.originfunctionThe innermost frame of stacktrace outside the test module: where the failing line actually is.
test.source.parse_framefunctionSplits one stack trace line into a Frame.
test.source.short_pathfunctionA path written relative to the working directory when it is under it, and left alone when it is not.
test.source.user_framesfunctionEvery frame of stacktrace that belongs to the code under test, innermost first.
test.spy_onfunctionReplaces target[key] with a recording stand-in that calls through to the original, and returns the Mock…
test.style.badgefunctionA filled, inverted label, the way a status banner reads in a CI log.
test.style.bluefunctionTodo entries and other informational notes.
test.style.boldfunctionEmphasis.
test.style.cyanfunctionStructure: suite names, headings.
test.style.dimfunctionDe-emphasis, for detail that should recede: timings, counts, the suite path above a failure.
test.style.enabledfunctionWhether colour is currently being emitted.
test.style.greenfunctionSuccess.
test.style.greyfunctionPunctuation and separators.
test.style.indentfunctionTwo spaces per level, the indent every nested suite and test line in the report is built from.
test.style.inversefunctionReserved for text that has to be found instantly on a busy screen.
test.style.magentafunctionValues in a rendered diff or failure message.
test.style.pad_leftfunctionPads text on the left to width visible columns, leaving it alone when it is already that wide or wider.
test.style.pad_rightfunctionPads text on the right to width visible columns, leaving it alone when it is already that wide or wider.
test.style.redfunctionFailure.
test.style.rulefunctioncount copies of character.
test.style.set_enabledfunctionForces colour on or off, overriding what the environment said.
test.style.set_unicodefunctionForces the Unicode symbol set on or off.
test.style.stripfunctiontext with every ANSI escape sequence removed.
test.style.symbolsfunctionThe symbol set in use, keyed by role.
test.style.truncatefunctionShortens text to at most width visible columns, marking the cut with an ellipsis.
test.style.unicodefunctionWhether the Unicode symbol set is in use, as opposed to the ASCII fallbacks.
test.style.visible_lengthfunctionHow many columns text occupies once its escape sequences are discounted.
test.style.whitefunctionPlain foreground, used to lift a key out of dimmed surroundings.
test.style.widthfunctionThe width to lay the report out to.
test.style.yellowfunctionSkipped and other deliberate non-results.
test.use_colorfunctionForces colour in the report on or off, overriding what the environment said.

Submodules

ModuleReached asSummary
test.conductimport test.conductRunning a directory of test files, each in a process of its own.
test.contextimport test.contextWhat is true right now, while one particular test is running.
test.diffimport test.diffStructural equality, and the rendering of what two unequal values disagree about.
test.errortest.error.*The errors the test module raises, kept in a module of their own so that the assertion side and the runner…
test.expecttest.expect.*expect(value) and everything that can be said about a value once you have one.
test.formatimport test.formatTurning any Zuri value into text a person can read in a failure message.
test.mocktest.mock.*Test doubles: functions that record how they were called, and stand-ins that replace a real one for the…
test.reportertest.reporter.*How a run is shown.
test.resulttest.result.*The shapes a test run is made of: the tree the declarations build, and what running it produced.
test.runnerimport test.runnerCollecting the tests a file declares, deciding which of them to run, and running them.
test.snapshotimport test.snapshotSnapshot testing: recording what a value looked like the first time and failing when it stops looking like…
test.sourceimport test.sourceWorking out where a failure came from, and showing the code that was there.
test.styleimport test.styleTerminal 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