Smoke
Runs tests against anything, using command-line arguments, STDIN, STDOUT and STDERR.
Install / Use
/learn @SamirTalwar/SmokeREADME
Smoke
An integration test framework for practically anything.

Smoke is designed to test anything that can be wrapped with a command-line interface. In practice, this amounts to almost any application or large piece of code. Whatever you're working on, no matter how big or complicated, you can usually wrap a CLI around it with minimum effort.
Smoke works especially well for testing large applications, especially after the fact. It allows you to create regression tests, golden master tests, and other things that make refactoring a legacy application much easier.
It's not a replacement for other, smaller tests. We recommend writing unit tests (perhaps even first), especially for new code.
Smoke is distributed under the MIT license.
Installation
You can download the latest release from the Releases page.
- The latest Windows release was built on Windows Server 2022.
- The latest macOS release was built on macOS 11 (Big Sur), on x86_64 hardware.
- If you need native arm64 support, you will need to build it yourself.
- The latest Linux release was built on Ubuntu 20.04, on x86_64 hardware.
- The binary depends on the dynamic libraries
glibcandgmp. - If you're running on a non-glibc-based OS such as Alpine Linux, you will either need to build it yourself or install both
gcompatandgmp. - If you need native arm64 support, you will need to build it yourself.
- The binary depends on the dynamic libraries
Building
You can also build it yourself, using either Nix or Stack.
With Nix:
- Install Nix.
- Run
nix-build -o ./out/build. - Find Smoke at
./out/build/bin/smoke.
With Stack:
- Install Stack.
- Run
stack install --local-bin-path=./out/build. - Find Smoke at
./out/build/smoke.
Writing Test Cases
A test case consists of input and expected output. It's made with a YAML file.
First off, you need to specify the command itself. The command is the program to be run (and any common arguments). It is executed from the current working directory. The command can be overriden for each individual test case too.
Input can come in two forms: standard input and command-line arguments.
- Command-line arguments are appended to the command to run it.
- Standard input is piped into the program on execution.
Outputs that can be observed by Smoke consist of standard output, standard error and the exit status of the program. These are captured by running the program, then compared to the expected values specified. Any difference results in a test failure.
- Expected standard output is compared with the actual standard output. Alternatively, multiple possible expected outputs can be specified. If there are multiple outputs, a match with any of them will be considered a success.
- Expected standard error works in exactly the same way as expected standard output.
- The expected exit status is a single number between
0and255.
At least one of standard output and standard error must be specified, though it can be empty. If no exit status is specified, it will be assumed to be 0.
Simple test cases
For a simple example, let's try testing a command-line calculator program.
Our simplest calculator test case looks like this. It's a specification file named smoke.yaml (the file basename is a convention; you can name it anything you want ending in .yaml).
command:
- ruby
- calculator.rb
tests:
- name: addition
stdin: |
2 + 2
stdout: |
4
That's it.
We use the YAML operator | to capture the following indented block as a string. This allows us to easily check for multiline output, and includes the trailing newline, which is useful when dealing with software that typically prints a newline at the end of execution. It also guarantees that we parse the value as a string, and not, for example, as a number, as in the case above.
We might want to assert that certain things fail. For example, postfix notation should fail because the second token is expected to be an operator. In this example, our calculator is expected to produce a semi-reasonable error message and exit with a status of 2 to signify a parsing error.
tests:
# ...
- name: postfix-notation-fails
stdin: |
5 3 *
exit-status: 2
stderr: |
"3" is not a valid operator.
Sometimes the response might be one of a few different values, in which case, we can specify an array of possible outcomes:
tests:
# ...
- name: square root
stdin: |
sqrt(4)
stdout:
- |
2
- |
-2
We don't always want to check the full output; sometimes checking that it contains a given substring is more useful. We can use the contains: operator to specify this:
tests:
# ...
- name: big multiplication
stdin: |
961748927 * 982451653
stdout:
contains: "1021"
Note that we don't use | here, as we don't want to capture the trailing newline, which would make this test fail. Instead we use quotes around the value to ensure that the YAML parser treats it as a string, not a number.
You can also use equals: to explicitly specify that we're checking equality, though this is the default.
We can use files to specify the STDIN, STDOUT or STDERR values:
tests:
- name: subtraction
stdin:
file: tests/subtraction.in
stdout:
file: tests/subtraction.out
Using files gives us one big advantage over specifying the content inline: if the tests fail, but the actual output looks correct, we can "bless" the new STDOUT or STDERR with the --bless flag. This means you don't have to spend time copying and pasting, and can instead just approve the results automatically.
We can ignore tests (temporarily, we hope) by adding ignored: true.
And, of course, you can combine all these techniques together.
Testing files
Smoke can also test that your application wrote a file.
For example, if we wanted to test that our implementation of the cp (copy) command worked, we could write the following:
working-directory: .
tests:
- name: copy a file
command:
- cp
args:
- input.file
- output.file
files:
- path: output.file
contents:
file: input.file
revert:
- .
You also need to create a file called input.file, containing whatever you want.
This test will run cp input.file output.file. In doing so, it will set the working directory to the same directory as the smoke.yaml file (so you can run the tests from anywhere, and they'll behave exactly the same). It will then revert the entire contents of the specified directory, ., to its state before the test was run.
As with STDOUT and STDERR, you can also use --bless to automatically accept the new output if it changes.
Take a look at the files fixture for more examples.
Running with a shell
Sometimes writing a small program for testing purposes is a bit distracting or over the top. We can specify a string as the contents of a command, which will be passed to the default shell (sh on Unix, cmd on Windows).
You can override the shell, too, both per test, or at the top-level, by providing a shell: section. For example, we could use this to pass the -e flag to bash, making sure it fails fast:
tests:
- name: use custom shell flags
command:
shell:
- bash
- -e
script: |
echo 'Something.' >&2
false
echo 'Something else.' >&2
exit-status: 1
stderr: |
Something.
You could even set your shell to python or ruby (or your favorite scripting language) to allow you to embed scripts of any kind in your Smoke specification.
You can find more examples in the local shell fixture and global shell fixture.
Setting environment variables
It is possible to set environment variables, both as defaults for a suite, and for a specific test. Environment variables inherit from the suite, and the suite inherits from the environment smoke is started in.
environment:
CI: "0"
tests:
- name:
environment:
CI: "1"
command: echo ${CI}
stdout: |
1
You can find more examples in the environment fixture.
Filtering output
Sometimes, things aren't quite so deterministic. When some of the output (or input) is meaningless, or if there's just too much, you can specify filters to transform the data.
[httpbin.org][] provides a simple set of HTTP endpoints that repeat what you tell them to. When I GET https://httpbin.org/get?foo=bar, the response body looks like this:
{
"args": {
"foo": "bar"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "HTTPie/1.0.0"
},
"origin": "1.2.3.4",
"url": "https://httpbin.org/get?foo=bar"
}
Unfortunately, because of the "origin", this isn't very testable, as that might as well be random data. Given that I only really care about the "args" property, I can use jq to just extract that p
Related Skills
node-connect
341.6kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
84.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
341.6kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
84.6kCommit, push, and open a PR
