In this lesson, you will learn how to improve the testability of your business logic by decoupling how errors are processed from the logic that generates them.
The Problem: Hardcoded Side Effects
When a function performs an action that might fail, we often reach for standard logging or telemetry inside the function body. However, hardcoding log::error! or a specific metric-tracking call creates a hidden dependency. This makes unit testing difficult: if your test environment doesn't have a configured logger, or if you want to verify that a specific error was reported without parsing log files, you are stuck.
Dependency Injection of Error Handlers
Instead of letting your logic dictate how an error is reported, inject an "Error Handler" trait. By passing a closure or a trait object into your function, the function focuses solely on execution, while the caller decides the consequence of failure.
Consider this modular approach:
trait ErrorReporter {
fn report(&self, err: &str);
}
// Our logic doesn't care how errors are reported
fn process_payment(amount: u32, reporter: &impl ErrorReporter) -> Result<(), ()> {
if amount == 0 {
reporter.report("Invalid amount");
return Err(());
}
Ok(())
}
Why this improves Testability
In your production code, you might pass a struct that sends errors to Sentry or ELK. In your unit tests, you can pass a "Mock" reporter that stores the error in a Vec<String>. This allows you to assert that your code reported the correct error without requiring a network connection or a global logging state.
#[cfg(test)]
struct MockReporter(std::cell::RefCell<Vec<String>>);
impl ErrorReporter for MockReporter {
fn report(&self, err: &str) {
self.0.borrow_mut().push(err.to_string());
}
}
By shifting the responsibility of error reporting to the caller, you create pure, deterministic functions that are significantly easier to test in isolation. You no longer rely on global state to verify that your system is communicating failures correctly.
Key Takeaway: Injecting error-handling behavior allows you to swap real-world side effects for lightweight mocks, making your business logic testable and decoupled from your reporting infrastructure.