When tests slow you down
You are refactoring a module. You write a test that validates the output against a real database. The test connects, runs a migration, queries the result, and asserts the schema. It takes forty seconds to run. You save your file. You run cargo test. You wait. You make another change. You run cargo test. You wait again. Your feedback loop grinds to a halt.
The temptation is to delete the test. Or comment it out. If you comment it out, the code stops compiling. If the API changes, you won't know until you uncomment it and hit a syntax error. If you delete it, you lose the safety net entirely. You need a way to keep the test in the codebase, keep it compiling, but tell the runner to skip it by default. You still want to run it when you are ready, or when you are on a CI runner that has the database available.
That is what #[ignore] does. It marks a test as skipped during normal runs. The test stays in the binary. It still compiles. It still catches syntax errors. But the test harness skips execution unless you explicitly ask for it.
The playlist analogy
Think of your test suite like a music playlist. Most tracks play every time you press play. These are your fast, reliable unit tests. Some tracks are "Bonus Tracks" that are long or require a specific venue. You don't want to hear them every time. You tag them as bonus. The default play skips them. When you have time, or when you are at the venue, you toggle the "Bonus Tracks" setting and hear them.
#[ignore] is the bonus track tag. cargo test is the player. By default, the player skips bonus tracks. You can change the settings to play only bonus tracks, or to play everything including bonus tracks.
Minimal example
Add the #[ignore] attribute to a test function. The attribute goes on the line before the function. Order does not matter technically, but the convention is to put #[test] first, then #[ignore]. This makes the primary purpose clear at a glance.
#[test]
fn fast_addition() {
// This test runs every time.
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn slow_integration() {
// This test is ignored by default.
// It still compiles.
// It still checks for syntax errors.
std::thread::sleep(std::time::Duration::from_secs(10));
assert_eq!(2 + 2, 4);
}
Run cargo test. The output shows the fast test passing. It lists the slow test as ignored. The build succeeds. The slow test did not run.
How the test harness works
The #[ignore] attribute does not remove code. It adds metadata. When you run cargo test, the compiler builds a test binary. This binary contains all your test functions, including ignored ones. The binary also contains a test harness generated by the libtest crate.
The harness runs a discovery phase. It scans the binary for functions marked with #[test]. It reads the metadata attached to each function. It builds a list of tests. Each test entry has a name, a function pointer, and a flag indicating whether it is ignored.
The execution phase filters this list. By default, the harness filters out tests where the ignore flag is true. It runs the remaining tests. If you pass flags to the harness, the filter changes.
This architecture has a critical implication. Ignored tests are compiled. If you have a typo inside an ignored test, the build fails. If you refactor a function and break the signature used by an ignored test, the build fails. #[ignore] skips execution, not compilation. This keeps ignored tests alive. They act as a compile-time check even when they don't run.
Treat ignored tests as living code. If they stop compiling, you know immediately. Do not let them rot.
Running ignored tests
You control the filter with command-line flags. The flags go after cargo test --. The double dash passes arguments to the test harness, not to cargo.
To run only ignored tests, use --ignored. This is useful when you want to run the slow suite separately.
cargo test -- --ignored
To run all tests, including ignored ones, use --include-ignored. This is useful for a full regression run.
cargo test -- --include-ignored
You can combine filters. To run a specific ignored test by name, pass the name after the flags.
cargo test -- --ignored slow_integration
This runs only slow_integration. It skips all other tests, ignored or not. The harness applies the name filter and the ignore filter together.
Realistic example
A common use case is an integration test that requires external state. The test might need a database, a network connection, or a specific environment variable. You want the test in the repo so teammates can run it when they have the setup. You don't want it to block the fast feedback loop for developers who are working offline.
/// Validates the database migration against a real instance.
/// Requires a running PostgreSQL server on localhost:5432.
#[test]
#[ignore]
fn test_db_migration_integrity() {
// Connect to the database.
// This will panic if the connection fails.
let conn = establish_connection();
// Run the migration.
run_migration(&conn);
// Assert the schema is correct.
let version = get_schema_version(&conn);
assert_eq!(version, "2024_05_20_initial");
}
fn establish_connection() -> DbConnection {
// Implementation details.
todo!()
}
fn run_migration(conn: &DbConnection) {
// Implementation details.
}
fn get_schema_version(conn: &DbConnection) -> String {
// Implementation details.
todo!()
}
The convention is to add a comment explaining why the test is ignored. Do not just add #[ignore] silently. Future you, or a teammate, needs to know the reason. Is it slow? Does it require a database? Is it flaky? The comment provides the context.
#[ignore] // Requires local DB instance
#[ignore] // Flaky on CI, investigating
This turns the attribute into documentation. It tells the reader that the ignore is intentional and temporary, or that it is a permanent constraint.
Pitfalls and errors
Ignored tests can hide problems if you are not careful. The biggest risk is bit rot. If you ignore a test and never run it, the test code can drift out of sync with the production code. You might refactor a function and update the test, but if the test is ignored, you might forget to update it. Eventually, you run the ignored test and it fails. Or worse, it passes but tests the wrong thing.
Run ignored tests regularly. Schedule a CI job that runs --include-ignored nightly. Or run them manually before merging a large feature. If you cannot run a test, consider deleting it. An ignored test that never runs is worse than no test. It gives false confidence.
Ignored tests still compile. This is a feature, but it can be a surprise. If you have a syntax error inside an ignored test, the build fails. You cannot "ignore" a syntax error. The compiler does not know about the ignore flag. It sees the code and checks it.
If you have a missing variable inside an ignored test, you get a standard error.
error[E0425]: cannot find value `missing_var` in this scope
The error code E0425 appears regardless of the ignore attribute. Fix the code to make the build pass. The ignore attribute only affects the test harness at runtime.
Another pitfall is forgetting to run ignored tests in CI. If your CI only runs cargo test, ignored tests never run. You might merge code that breaks an ignored test, and no one notices. Configure your CI to run ignored tests. You can run them in a separate job to keep the fast feedback loop fast.
# Example GitHub Actions job
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo test
test-ignored:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo test -- --include-ignored
This ensures ignored tests run on every push. They might take longer, but they run. If an ignored test fails, the build fails. This keeps the suite honest.
Decision matrix
Use #[ignore] when a test is slow, requires external resources, or is temporarily broken but you want to keep the code compiling. Use #[ignore] when you need to skip a test locally but run it in CI, or vice versa. Use #[ignore] when the test depends on environment variables that are not available in all contexts.
Use #[cfg(test)] when the test code depends on types or functions that do not exist outside of test builds. Use #[cfg(test)] when you want to exclude the code entirely from the binary size. Use #[cfg(test)] for helper functions that are only used by tests.
Use environment variables or feature flags when you need to toggle tests based on configuration rather than hardcoding the skip. Use feature flags when the test requires optional dependencies. Use environment variables when the test behavior changes based on runtime settings.
Use #[should_panic] when the expected behavior is a panic, not a skip. Use #[should_panic] to test error handling paths that result in panics.
Do not use #[ignore] to hide failing tests. Fix the test or delete it. #[ignore] is for logistics, not for lying. If a test fails, the ignore attribute does not fix the bug. It just hides the symptom.