The following focuses on writing an integration test. However, writing unit tests is also encouraged!
Cargo has a wide variety of integration tests that execute the cargo binary and verify its behavior, located in the testsuite directory. The support crate and snapbox contain many helpers to make this process easy.
There are two styles of tests that can roughly be categorized as
These tests typically work by creating a temporary “project” with a Cargo.toml file, executing the cargo binary process, and checking the stdout and stderr output against the expected output.
Generally, a functional test will be placed in tests/testsuite/<command>.rs and will look roughly like:
use crate::prelude::*; use cargo_test_support::str; use cargo_test_support::project; #[cargo_test] fn <description>() { let p = project() .file("src/main.rs", r#"fn main() { println!("hi!"); }"#) .build(); p.cargo("run --bin foo") .with_stderr_data(str![[r#" [COMPILING] foo [..] [FINISHED] [..] [RUNNING] `target/debug/foo` "#]]) .with_stdout_data(str![["hi!"]]) .run(); }
The #[cargo_test] attribute is used in place of #[test] to inject some setup code and declare requirements for running the test.
ProjectBuilder via project():
Cargo.toml manifest using file(), one is automatically created with a project name of foo using basic_manifest().Execs via p.cargo(...):
support::compare for an explanation of the string pattern matching. Patterns are used to make it easier to match against the expected output.Tests often to need to verify Cargo created/removed files. The CargoPathExt trait (implemented by Path and PathBuf) provides a assert_dir_layout() to verify the files in a directory (including nested directories). This takes a snapshot of file paths for the given directory and asserts that all files are present and no new files have been created. This function also takes a list of patterns to ignore from the snapshot to make working with platform specific files easier.
Note: You will commonly need to call unordered() before passing your snapshot to deal with platform differences like binaries having .exe on Windows. assert_build_dir_layout is a more specialized version of assert_dir_layout() that is automatically unordered and ignores common platform specific files designed for the Cargo build cache.
If you are testing a Cargo feature that only works on “nightly” Cargo, then you need to call masquerade_as_nightly_cargo on the process builder and pass the name of the feature as the reason, like this:
p.cargo("build").masquerade_as_nightly_cargo(&["print-im-a-teapot"])
If you are testing a feature that only works on nightly rustc (such as benchmarks), then you should use the nightly option of the cargo_test attribute, like this:
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
This will cause the test to be ignored if not running on the nightly toolchain.
You should not write any tests that use the network such as contacting crates.io. Typically, simple path dependencies are the easiest way to add a dependency. Example:
let p = project() .file("Cargo.toml", r#" [package] name = "foo" version = "1.0.0" [dependencies] bar = {path = "bar"} "#) .file("src/lib.rs", "extern crate bar;") .file("bar/Cargo.toml", &basic_manifest("bar", "1.0.0")) .file("bar/src/lib.rs", "") .build();
If you need to test with registry dependencies, see support::registry::Package for creating packages you can depend on.
If you need to test git dependencies, see support::git to create a git dependency.
There are some utilities to help support tests that need to work against a target other than the host. See Running cross tests for more an introduction on cross compilation tests.
Tests that need to do cross-compilation should include this at the top of the test to disable it in scenarios where cross compilation isn't available:
if crate::utils::cross_compile::disabled() { return; }
The name of the target can be fetched with the cross_compile::alternate() function. The name of the host target can be fetched with cargo_test_support::rustc_host().
If the test needs to run the cross-compiled binary, then it should have something like this to exit the test before doing so:
if crate::utils::cross_compile::can_run_on_host() { return; }
UI Tests are a bit more spread out and generally look like:
tests/testsuite/<command>/mod.rs:
mod <case>;
tests/testsuite/<command>/<case>/mod.rs:
use crate::prelude::*; use cargo_test_support::compare::assert_ui; use cargo_test_support::current_dir; use cargo_test_support::file; use cargo_test_support::Project; #[cargo_test] fn case() { let project = Project::from_template(current_dir!().join("in")); let project_root = project.root(); let cwd = &project_root; snapbox::cmd::Command::cargo_ui() .arg("run") .arg_line("--bin foo") .current_dir(cwd) .assert() .success() .stdout_matches(file!("stdout.log")) .stderr_matches(file!("stderr.log")); assert_ui().subset_matches(current_dir!().join("out"), &project_root); }
Then populate
tests/testsuite/<command>/<case>/in with the project's directory structuretests/testsuite/<command>/<case>/out with the files you want verifiedtests/testsuite/<command>/<case>/stdout.log with nothingtests/testsuite/<command>/<case>/stderr.log with nothing#[cargo_test]:
This is used in place of #[test]
This attribute injects code which does some setup before starting the test, creating a filesystem “sandbox” under the “cargo integration test” directory for each test. The directory for each test is based on the integration test name, module (if there is one), and function name[^1]:
/path/to/cargo/target/tmp/cit/<integration test>/<module>/<fn name>/
The sandbox will contain a home directory that will be used instead of your normal home directory
Project:
Command via Command::cargo_ui():
OutputAssert via Command::assert():
CommandAssert via assert_ui():
The project, stdout, and stderr snapshots can be updated by running with the SNAPSHOTS=overwrite environment variable, like:
$ SNAPSHOTS=overwrite cargo test
Be sure to check the snapshots to make sure they make sense.
If you are testing a Cargo feature that only works on “nightly” Cargo, then you need to call masquerade_as_nightly_cargo on the process builder and pass the name of the feature as the reason, like this:
snapbox::cmd::Command::cargo() .masquerade_as_nightly_cargo(&["print-im-a-teapot"])
If you are testing a feature that only works on nightly rustc (such as benchmarks), then you should use the nightly option of the cargo_test attribute, like this:
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
This will cause the test to be ignored if not running on the nightly toolchain.
When checking output, use / for paths even on Windows: the actual output of \ on Windows will be replaced with /.
Be careful when executing binaries on Windows. You should not rename, delete, or overwrite a binary immediately after running it. Under some conditions Windows will fail with errors like “directory not empty” or “failed to remove” or “access is denied”.
On Windows, to avoid path length limitations, the tests use the following directory structure instead:
/path/to/cargo/target/tmp/cit/t123/
In some cases, you may need to dig into a test that is not working as you expect, or you just generally want to experiment within the sandbox environment. The general process is:
Build the sandbox for the test you want to investigate. For example:
cargo test --test testsuite -- features2::inactivate_targets.
In another terminal, head into the sandbox directory to inspect the files and run cargo directly.
The first test's sandbox directory is called t0.
cd target/tmp/cit/t0
Set up the environment so that the sandbox configuration takes effect:
export CARGO_HOME=$(pwd)/home/.cargo
Most tests create a foo project, so head into that:
cd foo
Run whatever cargo command you want. See Running Cargo for more details on running the correct cargo process. Some examples:
/path/to/my/cargo/target/debug/cargo checklldb or gdb:lldb /path/to/my/cargo/target/debug/cargob generate_root_unitsr check[^1]: Windows uses a separate directory layout, see Platform-Specific Notes for more details.