Skip to content
rusthacks.dev
CLI

Testing Rust Command Line Tools With Assert Cmd Step by Step

Test a CLI end to end with assert_cmd and predicates: run the real binary, check the exit status, stdout and stderr, feed stdin and use temporary files.

2 min read
Summarize in AI
Flat illustration for the post “Testing Rust Command Line Tools With Assert Cmd Step by Step”, composed around testing.

Unit tests prove the parts work. They do not prove the program prints the right thing when run as tool --lines notes.txt, handles a missing file with exit code 1, or fails on a flag typo with a usage error. assert_cmd closes that gap by running the real binary and describing what it did. Aim for a few end-to-end tests and many unit tests, a split described in testing in Rust. It is the testing step of the command line tool series.

assert_cmd 2, predicates 3, tempfile 3, Rust 1.90.

Dependencies

Cargo.tomlTOML
[dev-dependencies]
assert_cmd = "2"
predicates = "3"
tempfile = "3"

The tests

tests/cli.rsrust
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;

#[test]
fn counts_lines_from_stdin() {
    Command::cargo_bin("wc-lite")
        .unwrap()
        .arg("--lines")
        .write_stdin("a\nb\nc\n")
        .assert()
        .success()
        .stdout("3\n");
}

#[test]
fn reads_a_file() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("notes.txt");
    fs::write(&path, "one two\nthree\n").unwrap();

    Command::cargo_bin("wc-lite")
        .unwrap()
        .arg(&path)
        .assert()
        .success()
        .stdout(predicate::str::contains("2 3"));
}

#[test]
fn missing_file_fails_with_a_message() {
    Command::cargo_bin("wc-lite")
        .unwrap()
        .arg("does-not-exist.txt")
        .assert()
        .failure()
        .code(1)
        .stderr(predicate::str::contains("does-not-exist.txt"));
}

Run cargo test. Each test spawns the binary, so they are slower than unit tests: expect tens of milliseconds each. The first checks piped input and exact stdout. The second shows a temp file fixture, deleted when dir drops. The third asserts the exit code and that the error names the file, which keeps users from getting a generic failure.

What to assert

CheckWhy
--help exits 0 and mentions the toolCatches broken argument definitions
Bad flag exits 2Confirms usage errors, set by clap
Normal run, exact stdoutThe contract with scripts
Failure, exit code and stderr textThe contract with people

Exact stdout comparisons are brittle for human formats. Prefer contains for text a person reads, and exact matches for machine formats.

Snapshots

For long output, compare against a stored snapshot instead of writing it into the test; see snapshot tests with insta.

Environment

Command inherits the environment of the test process. Set .env("NO_COLOR", "1"), .env_remove("MYTOOL_CONFIG") and .current_dir(dir.path()) so a developer's machine settings cannot change the result.

Limits

These tests cannot reach private functions, and they cannot test interactive behavior that needs a terminal. For prompts, use a pseudo terminal crate or keep the logic testable. The crate documentation is on docs.rs for assert_cmd, and the Command Line Applications in Rust book has a testing chapter.

Also inTesting
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.