Integration Testing
Testing Public APIs
Testing public APIs through integration tests verifies not only runtime behavior, but also the ergonomics, visibility, and type signatures of your library's exported surface from the perspective of a downstream crate user.
Internal Crate View (`src/`):
- `pub(crate) struct RawBuffer` (Visible inside crate)
- `struct HelperState` (Visible inside module)
- `pub struct Client` (Visible everywhere)
External Consumer View (`tests/`):
- Only `pub` items reachable through a chain of `pub mod` or `pub use`
from `src/lib.rs` can be named or called!
Catching Accidental Visibility Gaps (pub vs pub(crate))
Unit tests inside src/ can accidentally rely on private struct fields, pub(crate) helper methods, or un-exported error types that an external caller cannot access. Writing an integration test in tests/ immediately catches missing pub exports or sealed builder workflows at compile time:
// ❌ does not compile
mod storage_crate {
pub struct Connection {
// Forgot `pub` on a method or constructor!
endpoint: String,
}
impl Connection {
pub(crate) fn open(endpoint: &str) -> Self {
Self {
endpoint: endpoint.to_string(),
}
}
}
}
fn main() {
// External caller (such as an integration test in `tests/`) cannot call `pub(crate)` items
let _conn = storage_crate::Connection::open("localhost:5432");
}
error[E0624]: associated function `open` is private
--> src/main.rs:19:43
|
9 | pub(crate) fn open(endpoint: &str) -> Self {
| ------------------------------------------ private associated function defined here
...
19 | let _conn = storage_crate::Connection::open("localhost:5432");
| ^^^^ private associated function
Verifying End-to-End Public Workflows and Trait Bounds
A thorough public API integration test exercises the complete lifecycle a real user writes:
- Constructing configurations using public builders or
Default. - Executing operations and matching on public error enums (
std::error::Error + Send + Sync). - Checking that public types implement expected standard traits (
Debug,Clone,PartialEq,Send,Sync).
pub mod kv_store {
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StoreError {
EmptyKey,
KeyNotFound(String),
}
impl fmt::Display for StoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StoreError::EmptyKey => write!(f, "key cannot be empty"),
StoreError::KeyNotFound(k) => write!(f, "key not found: {k}"),
}
}
}
impl std::error::Error for StoreError {}
#[derive(Debug, Default, Clone)]
pub struct MemoryStore {
map: HashMap<String, String>,
}
impl MemoryStore {
pub fn new() -> Self {
Self::default()
}
pub fn put(&mut self, key: impl Into<String>, value: impl Into<String>) -> Result<(), StoreError> {
let key = key.into();
if key.is_empty() {
return Err(StoreError::EmptyKey);
}
self.map.insert(key, value.into());
Ok(())
}
pub fn get(&self, key: &str) -> Result<&str, StoreError> {
self.map
.get(key)
.map(String::as_str)
.ok_or_else(|| StoreError::KeyNotFound(key.to_string()))
}
}
}
// Compile-time assertion helper often used in integration tests
fn assert_send_sync<T: Send + Sync>() {}
fn main() {
use kv_store::{MemoryStore, StoreError};
// 1. Verify thread-safety trait bounds on public types
assert_send_sync::<MemoryStore>();
assert_send_sync::<StoreError>();
// 2. Exercise public workflow
let mut store = MemoryStore::new();
store.put("region", "ap-south-1").unwrap();
assert_eq!(store.get("region"), Ok("ap-south-1"));
assert_eq!(store.put("", "val"), Err(StoreError::EmptyKey));
assert_eq!(
store.get("missing"),
Err(StoreError::KeyNotFound("missing".to_string()))
);
println!("Public API contract verified for region={:?}", store.get("region"));
}
Public API contract verified for region=Ok("ap-south-1")