Integration Tests
Integration Tests
Level 8 — Testing & Documentation Tests in the
tests/directory; each file is compiled as a separate crate.
1. Prerequisites
pubVisibility — The access modifier that Integration Tests rely on.- Crate — Because every integration test file is secretly compiled as its own independent crate!
2. Term Category
Rust Tooling (the external perspective): Unit tests live directly inside your src/ folder alongside your code. Because they are internal, they can see your private functions and test your internal plumbing.
Integration Tests live outside your codebase entirely, in a special tests/ folder at the root of your project. They are entirely external. They can only see the pub (public) API of your library, forcing you to test your code exactly the way a customer would use it.
3. Explanation
(1) Design Motivation — "Why did we design this?"
The Rust designers recognized two fundamentally different types of testing that both needed first-class support.
- You need Unit Tests (internal) to verify that the individual gears and cogs in your machine are perfectly machined and mathematically correct.
- You need Integration Tests (external) to verify that the entire machine works as expected when an actual user turns the key.
If you put Integration Tests inside your src/ folder, you might accidentally "cheat" by accessing private internal variables that a real user wouldn't have access to. By forcing Integration Tests into a separate tests/ directory and compiling them as completely independent crates, Rust mathematically guarantees that your tests cannot cheat.
(2) Reality Metaphor
Imagine you are opening a new Restaurant.
Unit Tests are the Head Chef standing in the kitchen, tasting the soup with a spoon to make sure it has enough salt (Internal Testing). The chef has full access to the pantry, the recipes, and the raw ingredients.
Integration Tests are a Secret Shopper walking through the front door of the restaurant (External Testing). The secret shopper sits at a table, orders from the public menu, and eats the final meal. The secret shopper isn't allowed to walk into the kitchen! They can only interact with the restaurant exactly the way a real customer would.
(3) Rust Code Examples
Short Snippet (The Folder Structure)
To write Integration Tests, you must step outside your src/ folder and create a new folder named tests/ at the root of your project (right next to Cargo.toml).
my_awesome_library/
├── Cargo.toml
├── src/
│ └── lib.rs <-- Your actual library code
└── tests/
└── my_tests.rs <-- Your integration tests!
Fuller Example (Writing the Test)
Unlike Unit Tests, you do not need to use #[cfg(test)] mod tests { ... }. Because the entire tests/ folder is only compiled when you run cargo test, Cargo already knows to keep it out of production!
File: tests/my_tests.rs
// 1. We must explicitly import our library, exactly like a customer would!
// (Assuming your Cargo.toml package name is `my_awesome_library`)
use my_awesome_library;
// 2. Just write your tests! No `mod tests` block needed.
#[test]
fn test_the_public_api() {
// We can only access functions marked with `pub` in our lib.rs!
let result = my_awesome_library::calculate_total(100);
assert_eq!(result, 120);
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Integration Tests Scoping and Lifecycle Rules
The mistake: Assuming Integration Tests instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("integration_tests_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("integration_tests_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Integration Tests State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Integration Tests through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Integration Tests Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Integration Tests instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Multi-Component Service Integration & Shared Fixture Test Harness
Scenario: Problem Requirements:
In production microservices built with Rust, integration testing requires validating multi-component workflows (such as an in-memory transactional Event Bus interacting with a User Account Service) strictly through public interfaces while sharing fixture setup patterns (mimicking tests/common/mod.rs).
Requirements:
- Define an
Eventenum representing domain events:UserCreated { id: u64, email: String }andUserDeleted { id: u64 }. - Define a thread-safe
EventListenertrait withfn on_event(&self, event: &Event) -> Result<(), String>. - Implement an
EventBusstruct that maintains registered listeners usingArc<Mutex<Vec<Box<dyn EventListener>>>>and dispatches published events to listeners. - Implement a
UserServicestruct that manages an internal databaseArc<RwLock<HashMap<u64, User>>>and publishes lifecycle events to theEventBus. - Create a
MockAuditLoggerstruct as an integration test fixture that records all received events into a thread-safe log vector. - Write a complete, compilable test module (
#[cfg(test)] mod tests) containing integration tests that check:- User creation triggers
UserCreatedevent dispatching and database persistence. - User deletion cleans up database state and triggers
UserDeletedevent. - Duplicate user creation attempts return an explicit
Errand emit no events.
- User creation triggers
- Use rigorous assertions (
assert_eq!,assert!).
Answer
Implementation
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
UserCreated { id: u64, email: String },
UserDeleted { id: u64 },
}
pub trait EventListener: Send + Sync {
fn on_event(&self, event: &Event) -> Result<(), String>;
}
#[derive(Default)]
pub struct EventBus {
listeners: Arc<Mutex<Vec<Box<dyn EventListener>>>>,
}
impl EventBus {
pub fn new() -> Self {
Self {
listeners: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn register(&self, listener: Box<dyn EventListener>) {
let mut guard = self.listeners.lock().unwrap();
guard.push(listener);
}
pub fn publish(&self, event: &Event) -> Result<usize, String> {
let guard = self.listeners.lock().unwrap();
let mut success_count = 0;
for listener in guard.iter() {
if listener.on_event(event).is_ok() {
success_count += 1;
}
}
Ok(success_count)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
pub id: u64,
pub email: String,
}
pub struct UserService {
db: Arc<RwLock<HashMap<u64, User>>>,
event_bus: Arc<EventBus>,
}
impl UserService {
pub fn new(event_bus: Arc<EventBus>) -> Self {
Self {
db: Arc::new(RwLock::new(HashMap::new())),
event_bus,
}
}
pub fn create_user(&self, id: u64, email: String) -> Result<User, String> {
let user = User {
id,
email: email.clone(),
};
let mut db_guard = self.db.write().unwrap();
if db_guard.contains_key(&id) {
return Err(format!("User ID {} already exists", id));
}
db_guard.insert(id, user.clone());
drop(db_guard);
let event = Event::UserCreated { id, email };
self.event_bus.publish(&event)?;
Ok(user)
}
pub fn delete_user(&self, id: u64) -> Result<(), String> {
let mut db_guard = self.db.write().unwrap();
if db_guard.remove(&id).is_none() {
return Err(format!("User ID {} not found", id));
}
drop(db_guard);
let event = Event::UserDeleted { id };
self.event_bus.publish(&event)?;
Ok(())
}
pub fn get_user(&self, id: u64) -> Option<User> {
let db_guard = self.db.read().unwrap();
db_guard.get(&id).cloned()
}
}
// Shared Integration Test Fixture (Simulating tests/common/mod.rs helper)
pub struct MockAuditLogger {
pub received_events: Arc<Mutex<Vec<Event>>>,
}
impl MockAuditLogger {
pub fn new() -> (Self, Arc<Mutex<Vec<Event>>>) {
let storage = Arc::new(Mutex::new(Vec::new()));
let logger = Self {
received_events: Arc::clone(&storage),
};
(logger, storage)
}
}
impl EventListener for MockAuditLogger {
fn on_event(&self, event: &Event) -> Result<(), String> {
let mut guard = self.received_events.lock().unwrap();
guard.push(event.clone());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_service_integration_with_event_bus() {
let event_bus = Arc::new(EventBus::new());
let (logger, event_store) = MockAuditLogger::new();
event_bus.register(Box::new(logger));
let user_service = UserService::new(Arc::clone(&event_bus));
// 1. Create user and verify returned domain object
let user = user_service.create_user(101, "alice@example.com".to_string()).unwrap();
assert_eq!(user.id, 101);
assert_eq!(user.email, "alice@example.com");
// 2. Verify state persistence through public query API
let fetched = user_service.get_user(101);
assert!(fetched.is_some());
assert_eq!(fetched.unwrap().email, "alice@example.com");
// 3. Verify external listener event delivery
let events = event_store.lock().unwrap();
assert_eq!(events.len(), 1);
assert_eq!(
events[0],
Event::UserCreated {
id: 101,
email: "alice@example.com".to_string()
}
);
drop(events);
// 4. Delete user and verify state cleanup & deletion event emission
let delete_res = user_service.delete_user(101);
assert!(delete_res.is_ok());
assert!(user_service.get_user(101).is_none());
let events = event_store.lock().unwrap();
assert_eq!(events.len(), 2);
assert_eq!(events[1], Event::UserDeleted { id: 101 });
}
#[test]
fn test_duplicate_user_creation_error_handling() {
let event_bus = Arc::new(EventBus::new());
let (logger, event_store) = MockAuditLogger::new();
event_bus.register(Box::new(logger));
let user_service = UserService::new(event_bus);
assert!(user_service.create_user(1, "user1@test.com".to_string()).is_ok());
let err_res = user_service.create_user(1, "user1@test.com".to_string());
assert!(err_res.is_err());
assert_eq!(err_res.unwrap_err(), "User ID 1 already exists");
// Verify no duplicate event was dispatched on failure
let events = event_store.lock().unwrap();
assert_eq!(events.len(), 1);
}
}
Step-by-Step Technical Explanation:
- Public API Contract: In integration testing, components communicate through
pubtraits (EventListener) and methods (create_user,delete_user,get_user). Private internals are omitted to test real caller behavior. - Shared Fixture Setup:
MockAuditLogger::new()provides a fixture that retains shared state viaArc<Mutex<Vec<Event>>>. This allows integration test assertions to query side effects without reaching into private service state. - Thread Safety & Mutability: Rust requires
Send + Syncbounds onBox<dyn EventListener>to allowEventBusto share listeners safely across threads.Arc<RwLock<...>>insideUserServiceguarantees concurrent read access while isolating exclusive write access during updates.
Exercise 2: Black-Box Integration Testing of HTTP API Middleware & Rate Limiting
Scenario: Problem Requirements: Web services require black-box integration testing to ensure that middleware layers (such as Authentication and Rate Limiting) execute correctly before request handlers are reached.
Requirements:
- Define a public
Requeststruct withpath: String,token: Option<String>, andclient_ip: String. - Define a public
Responsestruct withstatus_code: u16andbody: String. - Define a
Middlewaretrait withfn handle(&self, req: &Request) -> Result<(), Response>. - Implement
AuthMiddlewarewhich inspectsreq.token. If missing or invalid, it returnsErr(Response)with401 Unauthorized. - Implement
RateLimiterMiddlewarewhich usesMutex<HashMap<String, usize>>to track client IP requests up tomax_requests. Exceeding the threshold returnsErr(Response)with429 Too Many Requests. - Implement an
ApiPipelinerouter struct that storesVec<Box<dyn Middleware>>and dispatches incoming requests through the middleware chain down to endpoint routes (/api/v1/resource). - Write a complete test suite (
#[cfg(test)] mod tests) testing:- Request authentication failure returns HTTP 401.
- Sequential requests from a single client IP trigger HTTP 429 when exceeding rate limits.
- Valid requests to unknown paths return HTTP 404.
Answer
Implementation
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Debug, Clone)]
pub struct Request {
pub path: String,
pub token: Option<String>,
pub client_ip: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub status_code: u16,
pub body: String,
}
pub trait Middleware: Send + Sync {
fn handle(&self, req: &Request) -> Result<(), Response>;
}
pub struct AuthMiddleware {
secret_token: String,
}
impl AuthMiddleware {
pub fn new(secret_token: &str) -> Self {
Self {
secret_token: secret_token.to_string(),
}
}
}
impl Middleware for AuthMiddleware {
fn handle(&self, req: &Request) -> Result<(), Response> {
match &req.token {
Some(token) if token == &self.secret_token => Ok(()),
_ => Err(Response {
status_code: 401,
body: "Unauthorized: Invalid or missing token".to_string(),
}),
}
}
}
pub struct RateLimiterMiddleware {
max_requests: usize,
request_counts: Mutex<HashMap<String, usize>>,
}
impl RateLimiterMiddleware {
pub fn new(max_requests: usize) -> Self {
Self {
max_requests,
request_counts: Mutex::new(HashMap::new()),
}
}
}
impl Middleware for RateLimiterMiddleware {
fn handle(&self, req: &Request) -> Result<(), Response> {
let mut counts = self.request_counts.lock().unwrap();
let count = counts.entry(req.client_ip.clone()).or_insert(0);
if *count >= self.max_requests {
Err(Response {
status_code: 429,
body: "Too Many Requests: Rate limit exceeded".to_string(),
})
} else {
*count += 1;
Ok(())
}
}
}
pub struct ApiPipeline {
middlewares: Vec<Box<dyn Middleware>>,
}
impl ApiPipeline {
pub fn new() -> Self {
Self {
middlewares: Vec::new(),
}
}
pub fn add_middleware(&mut self, middleware: Box<dyn Middleware>) {
self.middlewares.push(middleware);
}
pub fn dispatch(&self, req: Request) -> Response {
for mw in &self.middlewares {
if let Err(resp) = mw.handle(&req) {
return resp;
}
}
match req.path.as_str() {
"/api/v1/resource" => Response {
status_code: 200,
body: "{\"data\": \"success\"}".to_string(),
},
_ => Response {
status_code: 404,
body: "Not Found".to_string(),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pipeline_authentication_failure() {
let mut pipeline = ApiPipeline::new();
pipeline.add_middleware(Box::new(AuthMiddleware::new("secret-key")));
let req = Request {
path: "/api/v1/resource".to_string(),
token: Some("wrong-key".to_string()),
client_ip: "192.168.1.1".to_string(),
};
let response = pipeline.dispatch(req);
assert_eq!(response.status_code, 401);
assert!(response.body.contains("Unauthorized"));
}
#[test]
fn test_pipeline_rate_limiting_enforcement() {
let mut pipeline = ApiPipeline::new();
pipeline.add_middleware(Box::new(AuthMiddleware::new("secret-key")));
pipeline.add_middleware(Box::new(RateLimiterMiddleware::new(2)));
let make_req = || Request {
path: "/api/v1/resource".to_string(),
token: Some("secret-key".to_string()),
client_ip: "10.0.0.5".to_string(),
};
// First two requests under limit return HTTP 200
let r1 = pipeline.dispatch(make_req());
assert_eq!(r1.status_code, 200);
let r2 = pipeline.dispatch(make_req());
assert_eq!(r2.status_code, 200);
// Third request exceeds threshold and returns HTTP 429
let r3 = pipeline.dispatch(make_req());
assert_eq!(r3.status_code, 429);
assert_eq!(r3.body, "Too Many Requests: Rate limit exceeded");
}
#[test]
fn test_pipeline_not_found_endpoint() {
let mut pipeline = ApiPipeline::new();
pipeline.add_middleware(Box::new(AuthMiddleware::new("secret-key")));
let req = Request {
path: "/api/v1/nonexistent".to_string(),
token: Some("secret-key".to_string()),
client_ip: "192.168.1.1".to_string(),
};
let response = pipeline.dispatch(req);
assert_eq!(response.status_code, 404);
assert_eq!(response.body, "Not Found");
}
}
Step-by-Step Technical Explanation:
- Short-Circuit Middleware Execution:
ApiPipeline::dispatchloops over trait objectsBox<dyn Middleware>. If any middleware returnsErr(Response), the pipeline immediately short-circuits and returns the error HTTP response without processing downstream handlers. - Stateful Rate Limiting:
RateLimiterMiddlewareprotects interior state withMutex<HashMap<String, usize>>. The test verifies client IP tracking across sequential request calls without needing internal struct inspection. - Black-Box API Assertions: The tests instantiate the pipeline via its public builder methods and submit
Requestvalues, verifying system behavior throughResponsestatus codes and payload strings (assert_eq!(response.status_code, 401)).
Exercise 3: Asynchronous Workflow Pipeline Integration & Fault Injection Testing
Scenario: Problem Requirements: Integration tests often need to verify transaction boundaries and fault tolerance when coordinating multiple external service traits (e.g. Payment Gateways and Notification Systems).
Requirements:
- Define a
PaymentResultenum with variantsSuccess { tx_id: String }andFailed { reason: String }. - Define a trait
PaymentGateway: Send + Syncwithfn charge(&self, account_id: &str, amount_cents: u64) -> PaymentResult. - Define a trait
NotificationService: Send + Syncwithfn notify(&self, account_id: &str, message: &str) -> Result<(), String>. - Create a
PaymentProcessorpipeline struct holdingArc<dyn PaymentGateway>andArc<dyn NotificationService>. - Implement
pub fn process_order(&self, account_id: &str, amount_cents: u64) -> Result<String, String>:- Calls
gateway.charge(). - If payment fails, returns an error immediately and does NOT call the notification service.
- If payment succeeds, attempts notification. If notification fails, returns a composite fault error.
- Calls
- Create mock implementations (
MockPaymentGateway,MockNotifier) with fault-injection flags (should_fail) and thread-safe record vectors. - Write a unit/integration test suite (
#[cfg(test)] mod tests) usingassert_eq!,assert!, and string matching to test successful processing, payment failure rollbacks, and notification fault injection.
Answer
Implementation
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaymentResult {
Success { tx_id: String },
Failed { reason: String },
}
pub trait PaymentGateway: Send + Sync {
fn charge(&self, account_id: &str, amount_cents: u64) -> PaymentResult;
}
pub trait NotificationService: Send + Sync {
fn notify(&self, account_id: &str, message: &str) -> Result<(), String>;
}
pub struct PaymentProcessor {
gateway: Arc<dyn PaymentGateway>,
notifier: Arc<dyn NotificationService>,
}
impl PaymentProcessor {
pub fn new(
gateway: Arc<dyn PaymentGateway>,
notifier: Arc<dyn NotificationService>,
) -> Self {
Self { gateway, notifier }
}
pub fn process_order(&self, account_id: &str, amount_cents: u64) -> Result<String, String> {
let result = self.gateway.charge(account_id, amount_cents);
match result {
PaymentResult::Success { tx_id } => {
let msg = format!("Payment of ${:.2} processed. Tx: {}", amount_cents as f64 / 100.0, tx_id);
match self.notifier.notify(account_id, &msg) {
Ok(_) => Ok(tx_id),
Err(notify_err) => Err(format!("Payment succeeded but notification failed: {}", notify_err)),
}
}
PaymentResult::Failed { reason } => Err(format!("Payment failed: {}", reason)),
}
}
}
// Fault Injection Mocks for Integration Testing
pub struct MockPaymentGateway {
pub should_fail: bool,
pub charges: Mutex<Vec<(String, u64)>>,
}
impl MockPaymentGateway {
pub fn new(should_fail: bool) -> Self {
Self {
should_fail,
charges: Mutex::new(Vec::new()),
}
}
}
impl PaymentGateway for MockPaymentGateway {
fn charge(&self, account_id: &str, amount_cents: u64) -> PaymentResult {
let mut guard = self.charges.lock().unwrap();
guard.push((account_id.to_string(), amount_cents));
if self.should_fail {
PaymentResult::Failed {
reason: "Insufficient funds".to_string(),
}
} else {
PaymentResult::Success {
tx_id: format!("TX-{}-{}", account_id, amount_cents),
}
}
}
}
pub struct MockNotificationService {
pub should_fail: bool,
pub notifications: Mutex<Vec<(String, String)>>,
}
impl MockNotificationService {
pub fn new(should_fail: bool) -> Self {
Self {
should_fail,
notifications: Mutex::new(Vec::new()),
}
}
}
impl NotificationService for MockNotificationService {
fn notify(&self, account_id: &str, message: &str) -> Result<(), String> {
if self.should_fail {
Err("SMS gateway unreachable".to_string())
} else {
let mut guard = self.notifications.lock().unwrap();
guard.push((account_id.to_string(), message.to_string()));
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_successful_payment_and_notification_workflow() {
let gateway = Arc::new(MockPaymentGateway::new(false));
let notifier = Arc::new(MockNotificationService::new(false));
let processor = PaymentProcessor::new(
Arc::clone(&gateway) as Arc<dyn PaymentGateway>,
Arc::clone(¬ifier) as Arc<dyn NotificationService>,
);
let res = processor.process_order("acc_123", 5000);
assert!(res.is_ok());
let tx_id = res.unwrap();
assert_eq!(tx_id, "TX-acc_123-5000");
// Verify payment gateway charge audit log
let charges = gateway.charges.lock().unwrap();
assert_eq!(charges.len(), 1);
assert_eq!(charges[0], ("acc_123".to_string(), 5000));
// Verify notification payload and dispatch log
let notifications = notifier.notifications.lock().unwrap();
assert_eq!(notifications.len(), 1);
assert_eq!(notifications[0].0, "acc_123");
assert!(notifications[0].1.contains("Payment of $50.00 processed"));
}
#[test]
fn test_payment_failure_prevents_notification() {
let gateway = Arc::new(MockPaymentGateway::new(true)); // Inject payment failure
let notifier = Arc::new(MockNotificationService::new(false));
let processor = PaymentProcessor::new(
Arc::clone(&gateway) as Arc<dyn PaymentGateway>,
Arc::clone(¬ifier) as Arc<dyn NotificationService>,
);
let res = processor.process_order("acc_456", 2500);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), "Payment failed: Insufficient funds");
// Verify notification service was NEVER invoked on payment failure
let notifications = notifier.notifications.lock().unwrap();
assert!(notifications.is_empty());
}
#[test]
fn test_notification_fault_injection_handling() {
let gateway = Arc::new(MockPaymentGateway::new(false));
let notifier = Arc::new(MockNotificationService::new(true)); // Inject notification fault
let processor = PaymentProcessor::new(
Arc::clone(&gateway) as Arc<dyn PaymentGateway>,
Arc::clone(¬ifier) as Arc<dyn NotificationService>,
);
let res = processor.process_order("acc_789", 10000);
assert!(res.is_err());
let err_msg = res.unwrap_err();
assert!(err_msg.contains("Payment succeeded but notification failed"));
assert!(err_msg.contains("SMS gateway unreachable"));
}
}
Step-by-Step Technical Explanation:
- Fault Injection Strategy: The mock structs store a
should_failboolean flag. In integration testing, this enables simulating network timeouts, gateway outages, or database errors without depending on unreliable external services. - Transaction Isolation & Invariant Verification:
test_payment_failure_prevents_notificationasserts that whencharge()returnsPaymentResult::Failed, the processor aborts immediately, keepingnotificationsempty (assert!(notifications.is_empty())). - Trait Abstraction for Dependency Injection: The
PaymentProcessorrelies onArc<dyn PaymentGateway>andArc<dyn NotificationService>trait objects. In production, real HTTP/gRPC client implementations are injected; in integration tests, mock structs are injected seamlessly.
6. Related Terms
- Crate — Every file in the
tests/folder is compiled as its own independent crate! - Doc Tests — Related concept: Doc Tests.
7. Key Takeaways
- Integration Tests live in a
tests/directory at the root of your project. - They act exactly like an external customer: they can only access your
pubAPI. - Every
.rsfile in thetests/folder is compiled as a completely separate crate. - You can only write integration tests for Library Crates (
lib.rs), not Binary Crates (main.rs). - You still use the
#[test]attribute, but you do not need#[cfg(test)].