Expert · Testing, tooling and unsafe Rust · topic 40 of 50 · 25 min
Integration tests and doc tests
Explanation
Two more kinds of test
Unit tests check small pieces from the inside. But you also need to know that the pieces work together, through the public interface other people actually call. And if you write examples in your documentation, you want them to stay correct. Rust handles both with built-in tools.
Integration tests: the outside view
my_shop/
├── Cargo.toml
├── src/
│ └── lib.rs // pub fn cart_total(...), pub struct Cart ...
└── tests/
├── checkout.rs // integration tests
└── common/
└── mod.rs // shared helpers (not a test file itself)// tests/checkout.rs
use my_shop::Cart;
#[test]
fn full_checkout_with_coupon() {
let mut cart = Cart::new();
cart.add("kurta", 1200);
cart.add("dupatta", 400);
cart.apply_coupon("FESTIVE10");
assert_eq!(cart.total(), 1440);
}Each file in tests/ is compiled as its own separate crate. It imports your library by its package name, and it can only see items marked pub, exactly like an outside user. If something is awkward to test from here, it is probably awkward to use too, which is useful feedback on your API design.
Helpers shared by several test files go in tests/common/mod.rs and are brought in with mod common;. Using the folder form stops cargo from treating the helper file as a test file of its own.
cargo test --test checkout # only tests/checkout.rs
cargo test --lib # only unit tests in the libraryDoc comments and doc tests
Comments starting with /// are documentation comments. They are written in Markdown and describe the item below them. cargo doc turns them into an HTML website like the one on docs.rs. Any code block inside them is compiled and run as a test by cargo test.
/// Adds GST to an amount in whole rupees.
///
/// # Examples
///
/// ```
/// let total = my_shop::with_gst(1000, 18);
/// assert_eq!(total, 1180);
/// ```
pub fn with_gst(amount: u32, rate: u32) -> u32 {
amount + amount * rate / 100
}cargo test --doc # run only the doc tests
cargo doc --open # build the docs and open them in the browserHow the three fit together
| Kind | Lives in | Sees | Checks |
|---|---|---|---|
| Unit test | src/*.rs, in mod tests | Private and public items | Small pieces of logic |
| Integration test | tests/*.rs | Public API only | Parts working together, as a user |
| Doc test | /// comments | Public API only | Examples in the docs stay correct |
Common mistakes
- Trying to write integration tests for a binary-only crate (just main.rs). Move the logic into src/lib.rs and keep main.rs thin.
- Putting helpers in tests/common.rs, which cargo runs as an empty test file. Use tests/common/mod.rs.
- Doc examples that only compile on your machine (absolute paths, network). Keep examples self-contained.
- Forgetting the crate name in doc examples: inside docs you must write my_shop::with_gst, as an outside user would.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Integration tests and doc tests · 1/6
Integration tests live in the tests folder. Each file is its own crate, so it can only use your public API.
Prefer a video by a teacher? Find free videos on “rust integration tests doc tests cargo doc” on YouTube ↗
In real life: A public library for Indian number formatting
A developer publishes a crate that formats numbers in the Indian system (12,34,567 instead of 1,234,567). Unit tests check the grouping logic inside. Integration tests call the public format_inr function exactly as users will. Doc tests in the README-style examples make sure every example shown on docs.rs really prints what it claims.
/// Formats a whole number with Indian digit grouping.
///
/// ```
/// assert_eq!(inr::group(1234567), "12,34,567");
/// assert_eq!(inr::group(999), "999");
/// ```
pub fn group(n: u64) -> String {
let s = n.to_string();
if s.len() <= 3 {
return s;
}
let (rest, last3) = s.split_at(s.len() - 3);
let mut parts: Vec<&str> = Vec::new();
let mut i = rest.len();
while i > 2 {
parts.push(&rest[i - 2..i]);
i -= 2;
}
parts.push(&rest[..i]);
parts.reverse();
format!("{},{}", parts.join(","), last3)
}Practice (0/3 done)
What can an integration test in tests/ access?
Where do you put helper functions shared by several integration test files?
Take a project with logic in src/lib.rs (or move it there). Add /// docs with a runnable example to two public functions, add tests/api.rs with three integration tests, and run cargo test, cargo test --doc and cargo doc --open.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Tooling: rustfmt, Clippy, docs and editionsTesting, tooling and unsafe Rust · same module
- Unsafe Rust: what it is and is notTesting, tooling and unsafe Rust · same module
- Integration testing: checking parts work togetherBackend Development · Testing and API documentation
- Integration tests and CIFlutter and Dart · Responsive design and testing
- Testing a full-stack appFull-Stack Development · Performance, security and testing
- Testing basics: unit, integration and end-to-endFrontend Web Development · Fast, accessible, findable and safe: quality for real sites
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Which comment style creates documentation for the item below it?
Answer: /// comment
/// is an outer doc comment.
What happens to code blocks inside /// comments when you run cargo test?
Answer: They are compiled and run as tests
Doc tests keep examples correct.
Why might a binary-only crate be hard to integration test?
Answer: Integration tests import a library crate; put the logic in lib.rs
Keep main.rs thin and put logic in the library.
Which command builds HTML documentation?
Answer: cargo doc
cargo doc builds docs; --open opens them.
Why commit Cargo.lock for an application?Related
Answer: It pins exact dependency versions so builds are repeatable
Cargo.lock records the exact versions used.
What does an integration test check that a unit test does not?Related
Answer: That several parts, like handler and database, work together
Integration tests check the joins between parts, where many real bugs live.
What does cargo-audit check?Related
Answer: Dependencies against a database of known vulnerabilities
It uses the RustSec advisory database.
Where does a cargo project keep its main code?Related
Answer: src/main.rs
cargo new puts the starting code in src/main.rs.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Testing, tooling and unsafe Rust” before its exam: Tooling: rustfmt, Clippy, docs and editions, Unsafe Rust: what it is and is not.
- Module 10 exam: Testing, tooling and unsafe Rust · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Real projects: CLI, web and WebAssembly. 50 topics left in the course before the final exam.