tl;dr: Testing shell scripts made easy. Run scripts with mocks, assertions, and coverage.
- Shell interpreter
- Mock external commands with specific outputs (stdout and stderr) and exit codes
- Mock environment variables
- Mock file existence
- Assert script's exit code and output (stdout and stderr)
- Assert calls of external commands
- Generate colored coverage output or Go coverage profiles.
Download and run the install script:
curl -sSfL https://github.com/feloy/tesh/releases/latest/download/install.sh | shOr install with Go:
go install github.com/feloy/tesh@latest
Or build from source:
go build -o tesh .The tesh command relies on the mvdan's Go sh library (https://github.com/mvdan/sh) to interpret and run shell scripts. tesh uses the many handlers provided by this interpreter to implement mocks, assertions and coverage.
The limitations of the interpreter are described in the library documentation (https://pkg.go.dev/mvdan.cc/sh/v3/interp):
Package interp implements an interpreter that executes shell programs. It aims to support POSIX, but its support is not complete yet. It also supports some Bash features.
The interpreter generally aims to behave like Bash, but it does not support all of its features.
The interpreter currently aims to behave like a non-interactive shell, which is how most shells run scripts, and is more useful to machines. In the future, it may gain an option to behave like an interactive shell.
To be sure that your scripts are executed in the exact same way they are tested, you can run the scripts "in production" with the tesh command:
Script (script.sh):
echo Hello WorldConsole:
$ tesh script.sh Hello World
You can provide a Scenario file to tesh with the --scenarios flag. This file defines one or more scenarios, and each scenario can define mocks for external commands among other things.
When scenarios only define mocks and no expectations, tesh behaves as a normal shell interpreter, except that instead of executing mocked sub-commands, it uses the exit code and stdout and stderr defined in the scenario.
In this mode, if several scenarios are defined in the Scenario file, and you do not scpecify a scenario ID, the interpreter executes the script with the first scenario; or you can specify the scenario to execute with the --scenario flag.
Script (examples/ex1.sh):
cat /path/to/file
Scenarios (examples/ex1.yaml):
scenarios: - id: file-exists description: file exists mocks: - description: the file /path/to/file exists command: cat args: - /path/to/file exit-code: 0 stdout: some text in the file - id: file-not-exists description: file does not exist mocks: - description: the file /path/to/file does not exist command: cat args: - /path/to/file exit-code: 1 stderr: the file /path/to/file does not exist
In this example, the script is executed with the first scenario, and because the sub-command cat /path/to/file is mocked, the interpreter does not call the cat command, but behaves as if the command had exited with the status code 0 and had written some text in the file in its stdout.
Console:
$ tesh examples/ex1.sh \ --scenarios examples/ex1.yaml some text in the file $ echo $? 0
In this example, the script is executed with the scenario file-exists, as in the previous example.
Console:
$ tesh examples/ex1.sh \
--scenarios examples/ex1.yaml \
--scenario file-exists
some text in the file
$ echo $?
0
In this example, the script is executed with the scenario file-not-exists. The sub-command cat /path/to/file being mocked, the interpreter does not call the cat command, but behaves as if the command had exited with the status code 1 and had written the file /path/to/file does not exist in its stderr. Because this sub-command is the latest one in the script and terminates with a status code 1, the script terminates with the status code 1.
Console:
$ tesh examples/ex1.sh \
--scenarios examples/ex1.yaml \
--scenario file-not-exists
the file /path/to/file does not exist
$ echo $?
1
A scenario can define one or more environment variables.
Script (examples/ex10.sh):
echo -n $MYVAR
Scenarios (examples/ex10.yaml):
scenarios: - id: env-not-set description: MYVAR env is not set - id: env-is-set description: MYVAR is set with myvalue envs: - MYVAR=myvalue
In this example, MYVAR is not defined, nothing is displayed.
Console:
$ tesh examples/ex10.sh \ --scenarios examples/ex10.yaml \ --scenario env-not-set
In this example, MYVAR is mocked by the scenario:
Console:
$ tesh examples/ex10.sh \ --scenarios examples/ex10.yaml \ --scenario env-is-set myvalue
In this example, MYVAR is defined and not mocked, its original value is displayed:
Console:
$ MYVAR=originalValue tesh examples/ex10.sh \ --scenarios examples/ex10.yaml \ --scenario env-not-set originalValue
In this example, MYVAR is defined, and is also mocked by the scenario; the value displayed is the one provided by the mock:
Console:
$ MYVAR=originalValue tesh examples/ex10.sh \ --scenarios examples/ex10.yaml \ --scenario env-is-set myvalue
A scenario can mock the existence of files.
Script (examples/ex11.sh):
[ -f ./path/to/file ] && echo -n "file exists" || echo -n "file does not exist"
Scenarios (examples/ex11.yaml):
scenarios: - id: file-exists description: file exists files: - path: ./path/to/file exists: true - id: file-not-exists description: file does not exist files: - path: ./path/to/file exists: false
In this example, the file is mocked as existing:
Console:
$ tesh examples/ex11.sh \ --scenarios examples/ex11.yaml \ --scenario file-exists file exists
In this example, the file is mocked as non existing:
Console:
$ tesh examples/ex11.sh \ --scenarios examples/ex11.yaml \ --scenario file-not-exists file does not exist
Scenarios can define assertions in addition to mocks. When all scenarios of a Scenario file provide assertions, the script is interpreted for each of the scenarios.
In this mode, all the scenarios are executed, and the exit code of tesh is 0 if and only if all assertions pass, or 1 otherwise. Also, the stdout and stderr for scenarios are discarded, and the result of the assertions are displayed instead.
In this example, the script is executed for all the scenarios of the Scenario file, and the results of the assertions are displayed:
Script (examples/ex2.sh):
cat /path/to/file
Scenarios (not complete, see complete file in ./examples/ex2.yaml):
scenarios: - id: file-exists description: file exists mocks: - description: the file /path/to/file exists command: cat args: - /path/to/file exit-code: 0 stdout: some text in the file expect: exit-code: 0 stdout: some text in the file stderr: ""
Console:
$ tesh examples/ex2.sh \ --scenarios examples/ex2.yaml Scenario: file-exists Scenario: file-not-exists Scenario: file-not-exists-failing-exit-code Exit Code: expected 0, actual 1 Scenario: file-not-exists-failing-stdout Stdout: expected "some wrong expect", actual "" Scenario: file-not-exists-failing-stderr Stderr: expected "some wrong stderr", actual "the file /path/to/file does not exist"
A scenario can assert that a sub-command has been called a specific number of times.
Script (examples/ex3.sh):
if ls /file/exists; then cat /file/exists fi
Scenarios (not complete, see complete file in ./examples/ex3.yaml):
scenarios: - id: file-exists description: file exists mocks: - description: the file /file/exists exists command: ls args: - /file/exists exit-code: 0 - description: the file /file/exists has a content command: cat args: - /file/exists exit-code: 0 stdout: some text in the file expect: exit-code: 0 stdout: some text in the file stderr: "" calls: - command: cat args: - /file/exists called: 1
Console:
$ tesh examples/ex3.sh \ --scenarios examples/ex3.yaml Scenario: file-exists Scenario: file-not-exists Scenario: file-not-exists-failing-call Call: cat [/file/exists], expected 1 calls, actual 0 calls Scenario: file-exists-failing-call Call: cat [/file/exists], expected 0 calls, actual 1 calls
tesh <script file> \
[--scenarios <scenarios file> [--scenario <scenario id>] ] \
[--coverage[=<file>]]
Flags:
--scenarios: YAML file defining scenarios. If omitted, the script is only executed.--scenario: Run a single scenario by id (requires--scenarios).--coverage: Show colored coverage on stdout (suppresses normal stdout/stderr).--coverage=<file>: Write Gocoverage.txtstyle output to the given file.
Top-level scenarios is a list of test scenarios. Each scenario supports:
id(string, required)description(string, optional)mocks(list) to fake external commandsenvs(list) to set environment variablesfiles(list) to fake file existenceexpect(object) for assertions
Sub-commands Mocks:
mocks: - description: the file /path/to/file exists command: cat args: [/path/to/file] exit-code: 0 stdout: some text in the file stderr: ""
Environment variables:
envs: - MYVAR=myvalue
File existence:
files: - path: ./path/to/file exists: true - path: ./path/to/other/file exists: false
Assertions:
expect: exit-code: 0 stdout: "hello" stderr: "" calls: - command: cat args: [/path/to/file] called: 1
Use --coverage to render the script with covered lines highlighted in green and uncovered in red.
Use --coverage=coverage.txt to create a Go coverprofile compatible file.
Coverage can be used in any mode (interpreter mode without scenarios, or with scenarios and with or without assertions).
Apache 2.0. See LICENSE.