An implementation of the Test Anything Protocol for unit testing Zsh scripts using pure Zsh
ZTAP allows you to test your Zsh scripts using pure Zsh. Use it to test anything: scripts, functions, plugins, whatever - all without leaving Zsh.
Here's an example to get you started:
@test "zsh has a place to call home" -d ${ZDOTDIR:-$HOME}
@test "the answer to life, the universe, and everything" $(( 6 * 7 )) -eq 42
@test "got root?" $USER = rootNow put that in a .zsh file and run it with ztap installed.
Behold, the TAP stream!
TAP version 13
ok 1 zsh has a place to call home
ok 2 the answer to life, the universe, and everything
not ok 3 got root?
---
operator: = (strings s1 and s2 are identical)
value1: mattmc3
value2: root
...
1..3
# pass 2
# fail 1Each test file runs inside its own subshell, so you can modify the global environment
without cluttering your session or breaking other tests. If all the tests pass, ztap
exits with a return code of 0, or 1 otherwise.
Install with a plugin manager:
- antidote:
antidote bundle mattmc3/ztap
Install manually:
# clone the repo
git clone --depth 1 https://github.com/mattmc3/ztap ~/.config/zsh/plugins/ztap
# source ztap in your .zshrc
source ~/.config/zsh/plugins/ztap/ztap3.zshTests are defined with the @test function. Each test begins with a description,
followed by a typical test expression. Refer to the test builtin
documentation for
operators and usage details.
@test <description> [<expected>] <operator> <actual>Often you have work that needs to happen before and after tests run like preparing the environment and cleaning up after you're done. The best way to do this is directly in your test file. Your tests are all written in Zsh, after all.
# setup
tmp=$(mktemp -d)
# run tests
() {
@test "file doesn't exist yet" ! -f $tmp/testfile
touch $tmp/testfile
@test "a file now exists" -f $tmp/testfile
}
# teardown
rm -rf $tmpFor more advanced setup/teardown operations, you may consider sourcing a common include file, or even writing reusable functions for setup/teardown operations.
When comparing multiline output you have a few options including
- collapse newlines using
echo - collect your input into an array
# You can use echo to collapse
@test "2,4,6,8! Who do we appreciate?" "$(echo $(seq 2 2 8))" = "2 4 6 8"
# collect output to a zsh array
arr=($(seq 10 1))
@test "Countdown!" "${arr[*]}" = "10 9 8 7 6 5 4 3 2 1"If you want to write to stdout while tests are running, use the @echo function.
It's equivalent to echo "# $argv", which prints a TAP comment.
@echo "=== example ==="Output to stderr is considered a warning by ZTAP. It could be an indicator of a syntax error in your tests, or it could be a normal part of the utility you are testing. It's best to always redirect stderr output and explicitly test for it.
# redirect stderr to stdout for success
output=($(source myutility.zsh 2>&1))
# test output is expected
expected="it's alive!"
@test "Is it alive?" "$expected" = "$output"If you are building a Zsh project and would like to use ZTAP to run tests for that
project, it can be helpful to include a simple test runner script. I recommend putting
a simple script in your project's ./bin/runtests file:
#!/usr/bin/env zsh
[[ -d .ztap ]] || git clone --depth 1 -q https://github.com/mattmc3/ztap .ztap
source ./.ztap/ztap3.zsh
ztap3 $@If you want something slightly more robust, use the following script:
#!/usr/bin/env zsh
# contents of ./tools/runtests in your project
0=${(%):-%N}
PROJECT_HOME=${0:a:h:h}
ZTAP_HOME=${ZTAP_HOME:-$PROJECT_HOME/.ztap}
[[ -d $ZTAP_HOME ]] ||
git clone --depth 1 -q https://github.com/mattmc3/ztap $ZTAP_HOME
source $ZTAP_HOME/ztap3.zsh
if (( $# )); then
ztap3 -c "$@"
else
ztap3 -c $PROJECT_HOME/tests/*.zsh
fiDon't forget to make your ./bin/runtests file executable:
chmod 755 ./bin/runtestsAlso, be sure to add .ztap/ to your .gitignore so that you don't check ZTAP into
your repo unintentionally.