Crate cli_test_dir

source ·
Expand description

This crate makes it easier to write integration tests for CLI applications. It’s based on the “workdir” pattern used by BurntSushi’s xsv and ripgrep crates, but packaged in an easy-to-reuse form.

To use this crate, add the following lines to your Cargo.toml file:

[dev-dependencies]
# You can replace "*" with the current version of this crate.
cli_test_dir = "*"

You should now be able to write tests in tests/tests.rs as follows:

use cli_test_dir::*;

#[test]
fn write_output_file() {
    let testdir = TestDir::new("myapp", "write_output_file");
    testdir.cmd()
        .arg("out.txt")
        .expect_success();
    testdir.expect_path("out.txt");
}

You can use any options from std::process::Command to invoke your program.

§Testing that the program ran successfully

To check that a command succeeds, we can write:

let testdir = TestDir::new("true", "true_succeeds");
let mut cmd = testdir.cmd();
cmd.expect_success();

But this test would fail:

// Fails.
let testdir = TestDir::new("false", "false_succeeds");
let testdir = TestDir::new("cmd", "false_succeeds");
let mut cmd = testdir.cmd();
cmd.expect_success();

§Testing that the program exited with an error.

Sometimes you want to test that a program fails to run successfully.

let testdir = TestDir::new("false", "false_fails");
let mut cmd = testdir.cmd();
cmd.expect_failure();

And as you would expect, this test would fail:

// Fails.
let testdir = TestDir::new("true", "true_fails");
let mut cmd = testdir.cmd();
cmd.expect_failure();

§File input and output

The src_path function can be used to build paths relative to the top-level of our crate, and expect_path can be used to make sure an output file exists:

let testdir = TestDir::new("cp", "cp_copies_files");
let mut cmd = testdir.cmd();
cmd
  .arg(testdir.src_path("fixtures/input.txt"))
  .arg("output.txt")
  .expect_success();
testdir.expect_path("output.txt");

We can also create the input file manually or look for specific contents in the output file if we wish:

let testdir = TestDir::new("cp", "cp_copies_files_2");
let mut cmd = testdir.cmd();
testdir.create_file("input.txt", "Hello, world!\n");
cmd
  .arg("input.txt")
  .arg("output.txt")
  .expect_success();
testdir.expect_contains("output.txt", "Hello");
testdir.expect_file_contents("output.txt", "Hello, world!\n");

There are also negative versions of these functions where useful:

let testdir = TestDir::new("cp", "negative_tests");
let mut cmd = testdir.cmd();
testdir.create_file("input.txt", "Hello, world!\n");
cmd
  .arg("input.txt")
  .arg("output.txt")
  .expect_success();
testdir.expect_does_not_contain("output.txt", "Goodbye");
testdir.expect_no_such_path("does_not_exist.txt");

§Standard input and output

We can also test standard input and output:

let testdir = TestDir::new("cat", "cat_passes_data_through");
let mut cmd = testdir.cmd();
let output = cmd
  .output_with_stdin("Hello\n")
  .expect_success();
assert_eq!(output.stdout_str(), "Hello\n");

If you wish, you can display a command’s output using tee_output:

let testdir = TestDir::new("cat", "tee_output_shows_output");
let mut cmd = testdir.cmd();
let output = cmd
  .output_with_stdin("Hello\n")
  // Show `stdout` and `stderr`.
  .tee_output()
  .expect_success();
assert_eq!(output.stdout_str(), "Hello\n");

Note that this will currently print out all of stdout first, then all of stderr, instead of interleaving them normally.

To see the output of tee_output, you will also need to invoke cargo as follows:

cargo test -- --nocapture

§Contributing

Your feedback and contributions are welcome! Please see GitHub for details.

Structs§

  • This code is inspired by the WorkDir pattern that BurntSushi uses to test CLI tools like ripgrep and xsv.

Traits§

  • Extension methods for std::process::Command.
  • We define expect_status on quite a few related types to support different calling patterns.
  • Extension methods for std::process::Output.
  • Display command output and return it for examination.