Testing across module boundaries
Integration testing verifies that different parts of your system work together correctly. In Rust, integration tests live in the tests/ directory and test your crate as an external user would.
Integration testing addresses:
// src/lib.rs - The library we're testing
pub mod auth {
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct User {
pub id: u64,
pub username: String,
pub email: String,
password_hash: String,
}
pub struct AuthService {
users: HashMap<u64, User>,
sessions: HashMap<String, u64>,
next_id: u64,
}
#[derive(Debug, PartialEq)]
pub enum AuthError {
UserNotFound,
InvalidPassword,
UsernameTaken,
InvalidEmail,
SessionExpired,
}
impl AuthService {
pub fn new() -> Self {
AuthService {
users: HashMap::new(),
sessions: HashMap::new(),
next_id: 1,
}
}
pub fn register(&mut self, username: &str, email: &str, password: &str)
-> Result<u64, AuthError>
{
// Check if username is taken
if self.users.values().any(|u| u.username == username) {
return Err(AuthError::UsernameTaken);
}
// Validate email
if !email.contains('@') {
return Err(AuthError::InvalidEmail);
}
let id = self.next_id;
self.next_id += 1;
self.users.insert(id, User {
id,
username: username.to_string(),
email: email.to_string(),
password_hash: Self::hash_password(password),
});
Ok(id)
}
pub fn login(&mut self, username: &str, password: &str)
-> Result<String, AuthError>
{
let user = self.users.values()
.find(|u| u.username == username)
.ok_or(AuthError::UserNotFound)?;
if Self::hash_password(password) != user.password_hash {
return Err(AuthError::InvalidPassword);
}
// Generate session token
let token = format!("session_{}_{}", user.id, rand_string());
self.sessions.insert(token.clone(), user.id);
Ok(token)
}
pub fn get_user_by_session(&self, token: &str) -> Result<&User, AuthError> {
let user_id = self.sessions.get(token)
.ok_or(AuthError::SessionExpired)?;
self.users.get(user_id)
.ok_or(AuthError::UserNotFound)
}
pub fn logout(&mut self, token: &str) -> bool {
self.sessions.remove(token).is_some()
}
fn hash_password(password: &str) -> String {
// Simple hash for demonstration (use bcrypt/argon2 in production!)
format!("hash_{}", password)
}
}
impl Default for AuthService {
fn default() -> Self {
Self::new()
}
}
fn rand_string() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_nanos();
format!("{:x}", nanos)
}
}
pub mod storage {
use std::collections::HashMap;
use std::io::{self, Read, Write};
use std::path::Path;
pub trait Storage {
fn get(&self, key: &str) -> Option<Vec<u8>>;
fn set(&mut self, key: &str, value: &[u8]) -> io::Result<()>;
fn delete(&mut self, key: &str) -> bool;
fn keys(&self) -> Vec<String>;
}
/// In-memory storage for testing
#[derive(Default)]
pub struct MemoryStorage {
data: HashMap<String, Vec<u8>>,
}
impl MemoryStorage {
pub fn new() -> Self {
Self::default()
}
}
impl Storage for MemoryStorage {
fn get(&self, key: &str) -> Option<Vec<u8>> {
self.data.get(key).cloned()
}
fn set(&mut self, key: &str, value: &[u8]) -> io::Result<()> {
self.data.insert(key.to_string(), value.to_vec());
Ok(())
}
fn delete(&mut self, key: &str) -> bool {
self.data.remove(key).is_some()
}
fn keys(&self) -> Vec<String> {
self.data.keys().cloned().collect()
}
}
}
pub mod app {
use crate::auth::{AuthService, AuthError};
use crate::storage::{Storage, MemoryStorage};
pub struct Application<S: Storage> {
pub auth: AuthService,
pub storage: S,
}
impl<S: Storage> Application<S> {
pub fn new(storage: S) -> Self {
Application {
auth: AuthService::new(),
storage,
}
}
pub fn store_user_data(&mut self, token: &str, key: &str, data: &[u8])
-> Result<(), String>
{
let user = self.auth.get_user_by_session(token)
.map_err(|e| format!("Auth error: {:?}", e))?;
let prefixed_key = format!("user_{}_{}", user.id, key);
self.storage.set(&prefixed_key, data)
.map_err(|e| format!("Storage error: {}", e))
}
pub fn get_user_data(&self, token: &str, key: &str)
-> Result<Option<Vec<u8>>, String>
{
let user = self.auth.get_user_by_session(token)
.map_err(|e| format!("Auth error: {:?}", e))?;
let prefixed_key = format!("user_{}_{}", user.id, key);
Ok(self.storage.get(&prefixed_key))
}
}
impl Default for Application<MemoryStorage> {
fn default() -> Self {
Self::new(MemoryStorage::new())
}
}
}
// tests/integration_test.rs - Integration tests
// Import the crate being tested
use my_crate::auth::{AuthService, AuthError};
use my_crate::storage::{Storage, MemoryStorage};
use my_crate::app::Application;
// Common test utilities module
mod common;
// Test the full auth workflow
#[test]
fn test_full_auth_workflow() {
let mut auth = AuthService::new();
// Register a new user
let user_id = auth.register("alice", "alice@example.com", "password123")
.expect("Registration should succeed");
assert!(user_id > 0);
// Login
let token = auth.login("alice", "password123")
.expect("Login should succeed");
assert!(!token.is_empty());
// Access user info via session
let user = auth.get_user_by_session(&token)
.expect("Should get user by session");
assert_eq!(user.username, "alice");
// Logout
assert!(auth.logout(&token));
// Session should be invalid after logout
let result = auth.get_user_by_session(&token);
assert_eq!(result.unwrap_err(), AuthError::SessionExpired);
}
#[test]
fn test_duplicate_registration() {
let mut auth = AuthService::new();
auth.register("bob", "bob@example.com", "pass1")
.expect("First registration should succeed");
let result = auth.register("bob", "other@example.com", "pass2");
assert_eq!(result.unwrap_err(), AuthError::UsernameTaken);
}
#[test]
fn test_invalid_login() {
let mut auth = AuthService::new();
auth.register("charlie", "charlie@example.com", "correct")
.expect("Registration should succeed");
// Wrong password
let result = auth.login("charlie", "wrong");
assert_eq!(result.unwrap_err(), AuthError::InvalidPassword);
// Wrong username
let result = auth.login("nobody", "password");
assert_eq!(result.unwrap_err(), AuthError::UserNotFound);
}
// Test storage integration
#[test]
fn test_storage_operations() {
let mut storage = MemoryStorage::new();
// Store data
storage.set("key1", b"value1").unwrap();
storage.set("key2", b"value2").unwrap();
// Retrieve data
assert_eq!(storage.get("key1"), Some(b"value1".to_vec()));
assert_eq!(storage.get("key2"), Some(b"value2".to_vec()));
assert_eq!(storage.get("nonexistent"), None);
// List keys
let keys = storage.keys();
assert!(keys.contains(&"key1".to_string()));
assert!(keys.contains(&"key2".to_string()));
// Delete
assert!(storage.delete("key1"));
assert!(!storage.delete("key1")); // Already deleted
assert_eq!(storage.get("key1"), None);
}
// Test application integration (auth + storage)
#[test]
fn test_application_user_data() {
let mut app = Application::default();
// Register and login
app.auth.register("david", "david@example.com", "pass")
.expect("Registration should succeed");
let token = app.auth.login("david", "pass")
.expect("Login should succeed");
// Store user data
app.store_user_data(&token, "preferences", b"dark_mode")
.expect("Should store data");
// Retrieve user data
let data = app.get_user_data(&token, "preferences")
.expect("Should retrieve data");
assert_eq!(data, Some(b"dark_mode".to_vec()));
// Different user shouldn't access this data
app.auth.register("eve", "eve@example.com", "pass2").unwrap();
let eve_token = app.auth.login("eve", "pass2").unwrap();
let eve_data = app.get_user_data(&eve_token, "preferences")
.expect("Query should succeed");
assert_eq!(eve_data, None); // Eve has no preferences stored
}
#[test]
fn test_unauthenticated_access() {
let mut app = Application::default();
// Try to store data without valid session
let result = app.store_user_data("invalid_token", "key", b"value");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Auth error"));
}
// Async integration test (if using async)
#[cfg(feature = "async")]
#[tokio::test]
async fn test_async_operation() {
// Async test code here
}
// Test with setup/teardown
struct TestContext {
app: Application<MemoryStorage>,
user_token: String,
}
impl TestContext {
fn new() -> Self {
let mut app = Application::default();
// Set up test user
app.auth.register("test_user", "test@example.com", "test_pass")
.expect("Setup: registration should succeed");
let token = app.auth.login("test_user", "test_pass")
.expect("Setup: login should succeed");
TestContext {
app,
user_token: token,
}
}
}
#[test]
fn test_with_context() {
let mut ctx = TestContext::new();
// Test using pre-configured context
ctx.app.store_user_data(&ctx.user_token, "test_key", b"test_value")
.expect("Should store data");
let data = ctx.app.get_user_data(&ctx.user_token, "test_key")
.expect("Should retrieve data");
assert_eq!(data, Some(b"test_value".to_vec()));
}
// tests/common/mod.rs - Shared test utilities
use my_crate::auth::AuthService;
use my_crate::app::Application;
use my_crate::storage::MemoryStorage;
/// Create an authenticated test application
pub fn create_authenticated_app() -> (Application<MemoryStorage>, String) {
let mut app = Application::default();
app.auth.register("test", "test@test.com", "password")
.expect("Registration failed");
let token = app.auth.login("test", "password")
.expect("Login failed");
(app, token)
}
/// Helper to assert that a result is an error containing a specific message
pub fn assert_error_contains<T, E: std::fmt::Debug>(
result: Result<T, E>,
expected_substring: &str,
) {
match result {
Ok(_) => panic!("Expected error, got Ok"),
Err(e) => {
let error_msg = format!("{:?}", e);
assert!(
error_msg.contains(expected_substring),
"Error '{}' should contain '{}'",
error_msg,
expected_substring
);
}
}
}
tests/ directory: Cargo automatically compiles as integration teststests/common/mod.rs for reusable helpers#[cfg(feature = "async")]tests/
āāā integration_test.rs # Main integration tests
āāā auth_tests.rs # Auth-specific tests
āāā storage_tests.rs # Storage-specific tests
āāā common/
āāā mod.rs # Shared utilities
| Aspect | Unit Tests | Integration Tests |
|--------|------------|-------------------|
| Location | src/.rs | tests/.rs |
| Scope | Single function/module | Multiple modules |
| Access | Public + private | Public only |
| Speed | Fast | Slower |
| Dependencies | Mocked | Real or mocked |
// DON'T: Test implementation details
#[test]
fn test_internal_hash_format() {
// This tests private implementation, not behavior
let hash = AuthService::hash_password("test");
assert!(hash.starts_with("hash_"));
}
// DO: Test observable behavior
#[test]
fn test_password_validation() {
let mut auth = AuthService::new();
auth.register("user", "user@example.com", "correct").unwrap();
assert!(auth.login("user", "correct").is_ok());
assert!(auth.login("user", "wrong").is_err());
}
Run this code in the official Rust Playground