Mehdi Akiki
Rust Failure Atlas / Cargo and dependencies

RFA-151 · Case file with fixtures · Case 123 of 694 · Cargo workspace evidence

Why cargo test Compiles Examples Even When Unit Tests Pass

Cargo builds examples by default during cargo test so they stay compilation-checked, although they are not executed unless configured as tests. Read the failing target, repair maintained examples, and use target filters only for diagnosis or deliberate package policy.

Reviewed
Rust
Cargo 1.98.1, Rust 1.98.1, edition 2024
Targets
all Cargo host targets
Profiles
test

Direct answer

What this Rust failure means

Why it happens
By default Cargo builds examples during cargo test so they remain compilation-checked, which makes an invalid example part of the package test result.
First discriminating check
Read the target name in Cargo's error output and compare cargo test with cargo test --lib before changing library test code.

When cargo test fails, my first instinct used to be searching for the failing test name. Sometimes there is no failing test. A different package target did not compile.

The fixture has one healthy library test. Its failing example imports an API that no longer exists. cargo test reports E0432 for examples/broken.rs and names example "broken" in the final error. The unit test result never gets a chance to make the whole command successful.

A Cargo package contains several targets

A package can define a library, binaries, examples, integration tests, and benchmarks. These are separate compilation targets even when they share one manifest and source repository.

The cargo test documentation describes two phases: build selected test targets, then run the resulting test executables and documentation tests. It also documents that examples are built by default to ensure they compile.

An ordinary example is usually not executed as a test. Compilation alone is enough to catch a stale import, changed type, missing feature, or invalid function call. That is what happens in this case.

I find the distinction useful:

compiled by cargo test != necessarily executed as a test

The package command is checking more than the functions annotated with #[test].

Read the target label in the error

Cargo normally tells me which target failed:

could not compile ... (example "broken")

That line often saves a lot of time. An error under src/lib.rs, src/bin, examples, tests, a doctest, or a build script belongs to a different compilation context. Features, crate names, environment variables, and available cfg values can differ too.

I use a narrow command to confirm the diagnosis:

cargo test --lib

If this succeeds while the unfiltered command fails, the library tests were not the problem. This filter is a diagnostic tool. It is not automatically the final repair, because CI and users may reasonably expect cargo test to validate the complete package.

Fix maintained examples with the API

The repaired example calls the current stable_value function. The unfiltered cargo test command then compiles the library and example, runs the unit test, and runs documentation tests successfully.

Examples are part of the product surface. People copy them into real projects, documentation links to them, and compiler checking keeps them honest. Letting an example rot while narrowing CI to --lib improves one dashboard but weakens the crate.

When I change a public API, I search examples/, doctests, benches, integration tests, and README code together. These consumers often explain the API more clearly than its implementation does.

Deliberate target selection is still valid

The Cargo target-selection reference lists filters such as --lib, --bins, --examples, and --test name. They are useful for fast local feedback and for CI jobs that deliberately divide responsibilities.

For example, one job may run library tests on several toolchains while another checks all targets on the minimum supported version. The complete workflow must still make coverage visible. I do not want a permanent blind spot hidden behind a fast command.

Some examples require credentials, native libraries, a special target, or a large optional dependency. Cargo's example target configuration supports fields such as required-features. That can prevent an example from being built unless its real prerequisites are selected.

I use required-features only when the example genuinely demonstrates that feature. Adding an artificial feature merely to avoid compilation turns the example into untested shelfware.

Examples may be configured as tests

In Cargo.toml, an [[example]] target can set test = true. Then the target participates differently and may contain a test harness. Ordinary examples default to test = false, yet Cargo still builds them during the default test selection.

This explains an apparently contradictory observation: “my example was not run, but it broke my tests.” The command's build set and execution set are related but not identical.

Doctests add another layer. Code in documentation is extracted, compiled, and often run. A package can therefore fail after unit tests finish because a documentation example is stale. Again, the target label tells me where to look.

My debugging sequence

When cargo test reports an error outside a test function, I do this:

  1. Preserve the full Cargo command, selected features, workspace flags, target, and toolchain.
  2. Read the final target label and the source path.
  3. Use cargo test --lib or another narrow filter to isolate, not conceal, the failing target.
  4. Re-run the target explicitly, such as cargo test --examples or cargo check --example broken.
  5. Repair code or accurately declare its required feature and platform contract.
  6. Return to the original unfiltered command.

In workspaces I also check whether --workspace, --all-targets, and default members change what is selected. “Cargo test passed” has little meaning without the selection context.

The broader engineering principle is that executable documentation is a consumer. A stale example failing cargo test is not Cargo being distracted; it is early evidence that the package story and the package API have diverged. I keep that signal, make the target obvious, and fix the example as part of the change.