RFA-141 · Case file with fixtures · Case 113 of 694 · Cargo workspace evidence
Why an Integration Test Cannot Import a cfg(test) Library Item
Cargo compiles each integration test as a separate crate linked against the library's ordinary dependency build. cfg(test) applies to the target currently compiled as a test, not automatically to its dependencies; expose intentional test support or test through the public API.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- Cargo library packages
- Profiles
- test
Direct answer
What this Rust failure means
- Why it happens
- Each integration test is a separate crate linked against the library's ordinary dependency build, while cfg(test) applies to the test target currently being compiled.
- First discriminating check
- Confirm whether the failing test lives under tests/ and inspect the library artifact as a dependency rather than assuming its unit-test configuration is reused.
The failing Cargo fixture puts fixture_name behind #[cfg(test)] in the library, then imports it from tests/integration.rs. cargo test reports E0432 and notes that the item was configured out.
A unit-test module inside src/lib.rs would see the helper. An integration test does not compile in that same crate instance.
Integration tests are separate crates
The Cargo target documentation explains that every file under tests/ is compiled as a separate executable crate. It links against the package's library and can use its public API.
During one cargo test, the library can be built in more than one role:
library as its own unit-test target -> cfg(test) is enabled
library as a dependency of tests/x.rs -> ordinary library configuration
tests/x.rs as a test target -> cfg(test) is enabled for x.rs
The last line does not transfer cfg(test) into dependencies. Conditional configuration belongs to the crate currently being compiled.
This separation is intentional. Integration tests exercise the library in approximately the form downstream crates receive, rather than gaining automatic access to its private unit-test world.
cfg removes the item before import resolution
The conditional compilation reference describes how cfg controls whether source is included. For the ordinary library build used by the integration test, cfg(test) is false, so fixture_name does not exist in that compiled crate.
Making the function pub does not override this. Visibility answers who may access an existing item. Configuration answers whether the item exists at all.
The diagnostic helpfully shows both facts: there is no item in the crate root, and source contains one that was configured out by #[cfg(test)].
Test public behaviour when possible
My first choice is often removing the helper dependency from the integration test. A true integration test should call the public operation that users call and observe its result at a stable boundary.
This keeps the test valuable during refactoring. If it imports internal builders, mutable globals, or private parsing stages, it can become a large unit test placed in another directory without gaining real integration coverage.
Unit tests remain useful for internal algorithms. I place them beside the module where cfg(test) and private access are available.
Intentional test support needs an intentional API
Sometimes external-style tests require a deterministic clock, fake transport, fixture builder, or fault-injection hook. The repaired fixture exposes the simple helper in the ordinary library build, so the integration crate imports it successfully.
For a real library, I choose among several designs:
- make a small generally useful constructor public;
- expose a
test-supportCargo feature and enable it explicitly for the test build; - put shared test-only code under
tests/commonwhen it does not need library internals; - create a separate test-support crate in the workspace;
- keep the check as a unit test if private access is the actual need.
A public helper can use #[doc(hidden)], but hidden documentation does not make it private or remove compatibility obligations. I do not expose broad internal state only to satisfy one test.
Features require care too
A test-support feature makes the configuration explicit, but Cargo features can unify across a dependency graph. Enabling one may change the library build used by other targets in the same command.
I design such a feature so enabling it adds controlled hooks rather than changing production semantics. I also prevent accidental publication dependencies on unpublished workspace-only helper crates.
Running cargo test --no-default-features, relevant feature combinations, and the packaged crate catches differences hidden by a rich workspace environment.
One command can build several different artifacts
This case is a useful reminder that “the crate during cargo test” is not one artifact. Unit tests, integration tests, examples, binaries, documentation tests, build scripts, and procedural macros can each have different targets and configurations.
When a cfg! print in one target appears to contradict another, I record the package, target kind, host or target triple, enabled features, and rustc command. The observations may come from different compilations that are both correct.
My debugging sequence
When an integration test cannot import a cfg(test) item, I do this:
- Identify whether the failing source is a unit test or a crate under
tests/. - Run Cargo verbosely and distinguish the library test build from the dependency build.
- Remove the
cfg(test)assumption from the integration crate. - Prefer testing public behaviour at the integration boundary.
- Add the smallest public, feature-gated, or separate test-support API when needed.
- Test packaged and feature-minimal forms so workspace configuration does not hide the boundary.
The broad principle is that configuration applies per compilation unit. Cargo may compile one source package several ways in one test command. Once I name the target that imports the item and the library artifact it links, the missing helper stops being mysterious.