Skip to main content

✍️ Test Syntax

File structure

Every ZUnit test file must begin with the ZUnit shebang:

#!/usr/bin/env zunit

The rest of the file contains @test, @setup, and @teardown blocks — each delimited with { and }.

@test blocks

#!/usr/bin/env zunit

@test 'my first test' {
assert 'hello' same_as 'hello'
}
  • The label in quotes is used in output and reports.
  • The body can contain any valid Zsh code.
  • A test passes when it exits with code 0.
  • A test fails when it exits with a non-zero code.

@setup and @teardown

@setup runs before each test in the file. @teardown runs after each test, even if the test fails.

#!/usr/bin/env zunit

@setup {
SOME_VAR='rainbows'
}

@teardown {
unset SOME_VAR
}

@test 'check SOME_VAR' {
assert $SOME_VAR same_as 'rainbows'
}

@test 'change SOME_VAR' {
SOME_VAR='unicorns'
assert $SOME_VAR same_as 'unicorns'
}

@test 'SOME_VAR is reset between tests' {
# @setup ran again, so SOME_VAR is 'rainbows' again
run assert $SOME_VAR same_as 'unicorns'
assert $state equals 1
}
info

@setup and @teardown scope is per-file, not per-suite. Each test file has its own independent @setup/@teardown.

Helper functions

These functions are available inside every @test, @setup, and @teardown block.

run

Runs a command and captures its output and exit code without failing the test:

@test 'run captures output' {
run echo 'hello world'

assert $state equals 0
assert "$output" same_as 'hello world'
assert "${lines[1]}" same_as 'hello world'
}

After run:

  • $state — the exit code of the command
  • $output — full stdout (and stderr) as a string
  • $lines — array of output lines

load

Sources a file relative to the test directory (or as an absolute path):

@test 'load a helper' {
load '_support/helpers'
assert $MY_HELPER same_as 'loaded'
}

ZUnit appends .zsh automatically if the file without the extension is not found.

pass, fail, error, skip

Explicit test outcome shortcuts:

@test 'explicit pass' {
pass
}

@test 'explicit fail' {
fail 'something went wrong'
}

@test 'explicit error' {
error 'unexpected condition' # exit code 78
}

@test 'skip conditionally' {
[[ -z $CI ]] && skip 'only runs in CI'
assert $CI is_not_empty
}
FunctionExit codeEffect
pass0Marks test as passed immediately
fail <msg>1Marks test as failed with message
error <msg>78Marks test as errored with message
skip <msg>48Marks test as skipped with reason