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.rs verify private helper functions like consume_digits or is_ident_char on tricky character edge cases.
  • Integration tests inside tests/end_to_end.rs pass complete multi-line source programs through the public compile_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" }