Integration Testing
Unit vs. Integration Tests
Unit tests and integration tests verify software at two complementary levels of granularity: unit tests live inside src/ and test individual functions or internal data structures in isolation, while integration tests live in the top-level tests/ directory and test your library crate as a completely separate external consumer.
+-------------------------------------------------------------------+
| Crate Under Test (`my_lib`) |
| src/lib.rs |
| +-------------------------------------------------------------+ |
| | pub fn public_workflow() { internal_step(); } | |
| | fn internal_step() { ... } | |
| | | |
| | #[cfg(test)] mod tests { | |
| | // UNIT TEST: compiled inside `my_lib` | |
| | // Can call both `public_workflow` AND `internal_step` | |
| | } | |
| +-------------------------------------------------------------+ |
+-------------------------------------------------------------------+
^
| Links only to `pub` API
+-------------------------------------------------------------------+
| Integration Test Crate (`tests/api.rs`) |
| use my_lib::public_workflow; |
| // INTEGRATION TEST: compiled as a separate crate |
| // Cannot access `internal_step` or `pub(crate)` items |
+-------------------------------------------------------------------+
Comparing Unit Tests and Integration Tests
| Dimension | Unit Tests (src/) |
Integration Tests (tests/) |
|---|---|---|
| File Location | Inside src/lib.rs, src/main.rs, or module files |
Top-level tests/*.rs directory next to src/ |
| Crate Boundary | Compiled inside the target crate | Each file in tests/*.rs is compiled as a separate crate |
Requires #[cfg(test)]? |
Yes (on mod tests to avoid compiling in normal builds) |
No (cargo only compiles tests/ during cargo test) |
| Visibility Access | pub, pub(crate), pub(super), and private items |
Only pub items exported from src/lib.rs |
| Supported Targets | Both library (lib.rs) and binary (main.rs) targets |
Library (lib.rs) targets (cannot import from main.rs directly) |
| Cargo Flag | cargo test --lib or cargo test --bins |
cargo test --test <integration_test_name> |
How Both Work Together on the Same Feature
Consider a token lexer and parser:
- Unit tests inside
src/lexer.rsverify private helper functions likeconsume_digitsoris_ident_charon tricky character edge cases. - Integration tests inside
tests/end_to_end.rspass complete multi-line source programs through the publiccompile_source()entrypoint and verify the final AST or bytecode output.
// Inside `src/lib.rs` of crate `kv_parser`
#[derive(Debug, PartialEq, Eq)]
pub struct Entry {
pub key: String,
pub value: String,
}
// Private helper accessible only to unit tests inside this module
fn normalize_key(raw: &str) -> String {
raw.trim().to_ascii_lowercase().replace(' ', "_")
}
pub fn parse_entry(line: &str) -> Option<Entry> {
let (k, v) = line.split_once('=')?;
let key = normalize_key(k);
if key.is_empty() {
return None;
}
Some(Entry {
key,
value: v.trim().to_string(),
})
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn normalizes_internal_keys() {
// Unit test calls private `normalize_key` directly
assert_eq!(normalize_key(" Max Connections "), "max_connections");
}
}
fn main() {
// Simulating an external integration test caller using only `pub` items:
let entry = parse_entry("Server Host = 127.0.0.1").unwrap();
assert_eq!(
entry,
Entry {
key: "server_host".to_string(),
value: "127.0.0.1".to_string(),
}
);
println!("Parsed public entry: {entry:?}");
}
Parsed public entry: Entry { key: "server_host", value: "127.0.0.1" }