Question
I have a Rust library that implements a lint plugin. I want to use compiletest for tests without making it a required dependency for normal library users. What is the correct way to declare a test-only dependency in Cargo.toml?
Short Answer
Rust projects use Cargo's [dev-dependencies] section for packages needed only while developing and testing the project. You will learn where to declare these dependencies, when Cargo compiles them, and how to keep test code separate from your published library API.
Concept
A test-only dependency is a crate your project needs to run tests, benchmarks, examples, or development tooling, but does not need when another project uses your library as a dependency.
In Cargo, place these crates under [dev-dependencies] in Cargo.toml:
[dev-dependencies]
compiletest = "0.11"
Use the crate name and version required by the tool's documentation. For some Rust compiler testing workflows, the published package may have a different package name, so verify it on crates.io or in the tool's setup guide.
This matters because your library's normal dependencies are part of its production build graph. If a test runner is declared under [dependencies], users of your library may unnecessarily download, compile, and be affected by that tool. A dev-dependency keeps the dependency associated with your repository's development tasks instead.
Cargo makes dev-dependencies available to:
- Unit tests written in
#[cfg(test)]modules. - Integration tests in the
tests/directory. - Benchmarks and examples when they use the dependency.
They are not available to ordinary library code compiled for downstream users.
Mental Model
Think of your Rust crate as a product shipped to customers and your test tools as equipment in the workshop.
[dependencies]are parts built into every shipped product.[dev-dependencies]are workshop tools used to inspect and test the product.
A customer using your library needs the product, not your test runner. Declaring compiletest as a dev-dependency keeps that workshop tool out of the product's normal dependency list.
Syntax and Examples
Declare production dependencies and development-only dependencies in separate Cargo manifest sections:
[package]
name = "my_lint_plugin"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1"
[dev-dependencies]
compiletest = "0.11"
Then use the development dependency from test code. For example, an integration-test harness can live in tests/compiletest.rs:
#[test]
fn run_compile_tests() {
// Configure and invoke compiletest according to the version's API.
// The exact API can differ between compiletest releases.
assert!(true);
}
Run tests with:
cargo test
Cargo reads [dev-dependencies], builds the required development crates, compiles your test targets, and runs them.
For a simple test helper crate, the pattern is the same:
[dev-dependencies]
pretty_assertions = "1"
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
#[test]
fn adds_numbers() {
assert_eq!(2 + 3, 5);
}
}
Step by Step Execution
Consider this project layout:
my_lint_plugin/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── compiletest.rs
Cargo.toml:
[package]
name = "my_lint_plugin"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
pretty_assertions = "1"
tests/compiletest.rs:
use pretty_assertions::assert_eq;
#[test]
fn test_configuration_name() {
let configured_name = "my_lint_plugin";
assert_eq!(configured_name, "my_lint_plugin");
}
When you run cargo test, Cargo performs these steps:
- It reads
Cargo.tomland seespretty_assertionsin[dev-dependencies]. - It resolves and builds
pretty_assertionsfor the test build. - It compiles your library.
- It compiles the integration test in
tests/. - The test can import
pretty_assertionsbecause it is a development dependency. - Cargo runs
test_configuration_name.
When another application adds your crate as a regular dependency, its normal build does not compile this integration test or need pretty_assertions.
Real World Use Cases
Test-only dependencies are useful whenever development needs tools that are not part of your library's runtime behavior:
- Compiler and UI tests: Run compile-fail, pass, or diagnostic-output test suites with a tool such as
compiletest. - Better assertions: Use crates that produce readable diffs when strings, JSON, or collections differ.
- Temporary files: Create isolated files and directories while testing parsers, configuration loaders, or code generators.
- Mock servers: Test an HTTP client against a local fake service rather than a real API.
- Property-based testing: Generate many input cases to test parsers, validators, or algorithms.
- Benchmarks: Add benchmark-only tooling without exposing it as a requirement for library users.
For a lint plugin, a compile-test harness can verify that example source files produce expected diagnostics or compile successfully.
Real Codebase Usage
In real Rust projects, developers usually combine [dev-dependencies] with a clear separation between production and test code.
Unit tests near the code they test
pub fn is_valid_rule_name(name: &str) -> bool {
!name.is_empty() && name.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
#[cfg(test)]
mod tests {
use super::is_valid_rule_name;
#[test]
fn accepts_underscores() {
assert!(is_valid_rule_name("unused_variable"));
}
}
The #[cfg(test)] module is compiled when Cargo builds tests. This is a natural place to use dev-dependencies.
Integration tests for public behavior
Files in tests/ are separate crates. They test the library through its public API:
use my_lint_plugin::is_valid_rule_name;
#[test]
fn rejects_spaces() {
assert!(!is_valid_rule_name("unused variable"));
}
Dedicated test harnesses
A project that uses compiler tests often creates one integration test that configures and runs the harness. Test input files then live in a directory such as tests/ui/ or tests/compile-fail/. Keep the harness-specific crate in [dev-dependencies] and keep the library's normal source free of imports from it.
Feature-dependent test tooling
If tests require an optional production feature, enable that feature when running the test command:
cargo test --features experimental-rules
This is different from making a testing crate a normal dependency. Features control optional parts of your package; dev-dependencies describe tools used during development.
Common Mistakes
Putting a test crate in [dependencies]
This works, but makes a test tool look like a normal runtime/build dependency:
[dependencies]
compiletest = "0.11" # Usually the wrong section for test tooling
Prefer:
[dev-dependencies]
compiletest = "0.11"
Importing a dev-dependency from normal library code
This library code is compiled for ordinary users, so it cannot rely on a dev-dependency:
// src/lib.rs
use pretty_assertions::assert_eq; // Incorrect for normal library code
Keep the import inside a test-only module or a test target:
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
#[test]
fn works() {
assert_eq!(1, 1);
}
}
Forgetting #[cfg(test)] for an internal test module
Without #[cfg(test)], test helpers may be compiled in normal builds:
mod tests {
// Not automatically test-only just because of the name.
}
Use:
#[cfg(test)]
mod tests {
// Compiled only for test builds.
}
Assuming dev-dependencies are available to downstream crates
A crate that depends on your library cannot import your dev-dependencies. Each package declares its own dependencies. If your public API truly requires another crate's types or functionality, it belongs in [dependencies] instead.
Using the wrong crate name or API
Testing tools can have similarly named packages, version-specific APIs, or special setup requirements. Confirm the exact package name, version, and harness configuration from the crate's documentation before copying an example.
Comparisons
| Cargo manifest section | Intended use | Available to normal library code? | Used for cargo test? |
|---|---|---|---|
[dependencies] | Code required by the library or binary in normal builds | Yes | Yes, when needed by the package |
[dev-dependencies] | Test, benchmark, example, and development tooling | No, not in ordinary builds | Yes |
[build-dependencies] | Crates used by build.rs | Only in build.rs | Yes, if the build script runs |
#[cfg(test)] and [dev-dependencies] solve related but different problems:
| Tool | What it controls |
|---|---|
[dev-dependencies] | Which external crates Cargo provides to development targets |
#[cfg(test)] | Which Rust code is compiled only while testing |
Use both together when test code imports a testing-only crate.
Cheat Sheet
# Cargo.toml
[dev-dependencies]
compiletest = "0.11"
// src/lib.rs
#[cfg(test)]
mod tests {
#[test]
fn example() {
assert_eq!(2 + 2, 4);
}
}
cargo test
- Use
[dev-dependencies]for test runners, assertion helpers, mocks, temporary-file helpers, and benchmark tools. - Use
[dependencies]only when normal library or binary code needs the crate. - Test code in
tests/can use dev-dependencies. - Unit-test modules should normally use
#[cfg(test)]. - A downstream crate does not inherit your dev-dependencies.
build.rsdependencies belong in[build-dependencies], not[dev-dependencies].
FAQ
How do I add a dependency only for Rust tests?
Add it under [dev-dependencies] in Cargo.toml, then run tests with cargo test.
Will users of my library download a dev-dependency?
Not as part of a normal dependency build of your library. Dev-dependencies are intended for your package's development targets rather than its normal library build.
Can unit tests use dev-dependencies?
Yes. Put the test code in a #[cfg(test)] module and import the crate there.
Can files in the tests/ directory use dev-dependencies?
Yes. Integration tests are development targets and can use crates listed in [dev-dependencies].
Should compiletest be a normal dependency for a lint plugin?
Usually no, if it is only used to run the plugin's test suite. Put it under [dev-dependencies]. Use a normal dependency only if production code imports it.
Is #[cfg(test)] enough by itself?
No. #[cfg(test)] controls whether code is compiled for tests; [dev-dependencies] tells Cargo which external testing crates are available.
When should I use [build-dependencies] instead?
Use it only for crates imported by build.rs. A test harness that runs through cargo test belongs in [dev-dependencies].
Mini Project
Description
Create a small rule-name validator library with a test-only assertion helper. The project demonstrates that a dev-dependency can improve test output without becoming part of the library's normal implementation.
Goal
Write and test a public function while keeping the assertion helper limited to development and test builds.
Requirements
Create a library crate with a public is_valid_rule_name function.
Accept names containing ASCII letters, digits, and underscores.
Reject empty names and names containing spaces or hyphens.
Declare pretty_assertions as a dev-dependency.
Write unit tests that use pretty_assertions::assert_eq!.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.