blob: a44893dd2d89df76fd7f3ce081f6bbeed73a516d [file] [view] [edit]
# Writing Tests
The following focuses on writing an integration test. However, writing unit
tests is also encouraged!
## Testsuite
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
- functional tests
- The fixture is programmatically defined
- The assertions may be in-source snapshots, hard-coded strings, or programmatically generated
- Easier to share in an issue as a code block is completely self-contained
- ui tests
- The fixture is file-based
- The assertions use file-backed snapshots that can be updated with an env variable
- Easier to review the expected behavior of the command as more details are included
- Easier to get up and running from an existing project
- Easier to reason about as everything is just files in the repo
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.
### Functional Tests
Generally, a functional test will be placed in `tests/testsuite/<command>.rs` and will look roughly like:
```rust,ignore
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][cargo_test attribute] is used in place of
`#[test]` to inject some setup code and declare requirements for running the
test.
[`ProjectBuilder`] via `project()`:
- Each project is in a separate directory in the sandbox
- If you do not specify a `Cargo.toml` manifest using `file()`, one is
automatically created with a project name of `foo` using `basic_manifest()`.
[`Execs`] via `p.cargo(...)`:
- This executes the command and evaluates different assertions
- See [`support::compare`] for an explanation of the string pattern matching.
Patterns are used to make it easier to match against the expected output.
#### Filesystem layout testing
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.
#### Testing Nightly Features
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:
```rust,ignore
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:
```rust,ignore
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
```
This will cause the test to be ignored if not running on the nightly toolchain.
#### Specifying Dependencies
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:
```rust,ignore
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.
#### Cross compilation
There are some utilities to help support tests that need to work against a
target other than the host. See [Running cross
tests](running.md#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:
```rust,ignore
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:
```rust,ignore
if crate::utils::cross_compile::can_run_on_host() {
return;
}
```
[`cross_compile::alternate()`]: https://github.com/rust-lang/cargo/blob/d58902e22e148426193cf3b8c4449fd3c05c0afd/crates/cargo-test-support/src/cross_compile.rs#L208-L225
[`cargo_test_support::rustc_host()`]: https://github.com/rust-lang/cargo/blob/d58902e22e148426193cf3b8c4449fd3c05c0afd/crates/cargo-test-support/src/lib.rs#L1137-L1140
### UI Tests
UI Tests are a bit more spread out and generally look like:
`tests/testsuite/<command>/mod.rs`:
```rust,ignore
mod <case>;
```
`tests/testsuite/<command>/<case>/mod.rs`:
```rust,ignore
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 structure
- `tests/testsuite/<command>/<case>/out` with the files you want verified
- `tests/testsuite/<command>/<case>/stdout.log` with nothing
- `tests/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`:
- The project is copied from a directory in the repo
- Each project is in a separate directory in the sandbox
[`Command`] via `Command::cargo_ui()`:
- Set up and run a command.
[`OutputAssert`] via `Command::assert()`:
- Perform assertions on the result of the [`Command`]
[`Assert`] via `assert_ui()`:
- Verify the command modified the file system as expected
#### Updating Snapshots
The project, stdout, and stderr snapshots can be updated by running with the
`SNAPSHOTS=overwrite` environment variable, like:
```console
$ SNAPSHOTS=overwrite cargo test
```
Be sure to check the snapshots to make sure they make sense.
#### Testing Nightly Features
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:
```rust,ignore
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:
```rust,ignore
#[cargo_test(nightly, reason = "-Zfoo is unstable")]
```
This will cause the test to be ignored if not running on the nightly toolchain.
### Platform-specific Notes
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/`
## Debugging tests
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:
1. Build the sandbox for the test you want to investigate. For example:
`cargo test --test testsuite -- features2::inactivate_targets`.
2. In another terminal, head into the sandbox directory to inspect the files and run `cargo` directly.
1. The first test's sandbox directory is called `t0`.
`cd target/tmp/cit/t0`
2. Set up the environment so that the sandbox configuration takes effect:
`export CARGO_HOME=$(pwd)/home/.cargo`
3. Most tests create a `foo` project, so head into that:
`cd foo`
3. 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 check`
* Using a debugger like `lldb` or `gdb`:
1. `lldb /path/to/my/cargo/target/debug/cargo`
2. Set a breakpoint, for example: `b generate_root_units`
3. Run with arguments: `r check`
[cargo_test attribute]: https://doc.rust-lang.org/nightly/nightly-rustc/cargo_test_macro/attr.cargo_test.html
[`testsuite`]: https://github.com/rust-lang/cargo/tree/master/tests/testsuite/
[`ProjectBuilder`]: https://github.com/rust-lang/cargo/blob/d847468768446168b596f721844193afaaf9d3f2/crates/cargo-test-support/src/lib.rs#L196-L202
[`Execs`]: https://github.com/rust-lang/cargo/blob/d847468768446168b596f721844193afaaf9d3f2/crates/cargo-test-support/src/lib.rs#L531-L550
[`support`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/lib.rs
[`support::compare`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/compare.rs
[`support::registry::Package`]: https://github.com/rust-lang/cargo/blob/d847468768446168b596f721844193afaaf9d3f2/crates/cargo-test-support/src/registry.rs#L311-L389
[`support::git`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/git.rs
[Running Cargo]: ../process/working-on-cargo.md#running-cargo
[`snapbox`]: https://docs.rs/snapbox/latest/snapbox/
[`Command`]: https://docs.rs/snapbox/latest/snapbox/cmd/struct.Command.html
[`OutputAssert`]: https://docs.rs/snapbox/latest/snapbox/cmd/struct.OutputAssert.html
[`Assert`]: https://docs.rs/snapbox/latest/snapbox/struct.Assert.html
[^1]: Windows uses a separate directory layout, see [Platform-Specific Notes](#platform-specific-notes)
for more details.