The Context
When building complex Rust applications, you quickly realize that testing a single “unit” is difficult if it is tightly coupled to other services. Unit testing aims to ensure each part of the software performs as intended in isolation. However, controllers and services often depend on traits that handle database access or external logic, making them hard to test without a real environment.
The Problem
If you try to run unit tests while hitting real dependencies, you violate a core principle: unit tests must not depend on external resources like databases or APIs. As seen in the foo_controller.rs example, testing an endpoint requires a way to simulate the behavior of the service layer without actually running it. Without a mocking framework, you would have to manually implement “fake” traits for every test scenario, leading to massive code duplication.
The Solution
Important Disclaimer: This is a Proof of Concept (PoC) built for a specific course need. If you find this code useful and decide to use it, you do so entirely at your own risk. The authors decline any responsibility for its use.
The mockall crate allows you to automatically generate mock objects from traits. In the provided sources, it is used to mock dependencies in controllers to validate HTTP behavior. By using the Arrange, Act, Assert (AAA) pattern, we can clearly define what a mock should return before executing the code.
Before we dive in, the full source code is available on GitHub: 👉 https://github.com/gabo-gil-playground/rust-poc-unit-and-integration-test-sample
Here are the core dependencies in Cargo.toml:
[dev-dependencies] # development dependencies - should not be exported
# mock dependencies
mockall = { version = "=0.14.0" }
# parametrized test cases dependencies
rstest = { version = "=0.26.1" }
# run test sequencially
serial_test = { version = "=3.3.1" }
# unit and integration tests util / tools dependencies
tower = { version = "=0.5.3", features = ["util", "timeout"] }
# http mock server for external API calls
wiremock = { version = "=0.6.5" }
Pro-tip #1: The Automock Attribute
The fastest way to use mockall is by adding #[automock] directly above your trait definition. This allows the compiler to generate a MockTraitName that you can use in your Arrange step to set expectations on method calls.
// src/service/foo_service.rs
#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait FooServiceTrait {
...
}
Pro-tip #2: Precise Expectations
When arranging your test, use .expect(“method_name”) followed by .returning(|args| …). This ensures that if your code calls a dependency with the wrong parameters, the test fails immediately, providing better “documentation” of the system’s expected behavior:
// src/controller/foo_controller.rs
...
let mut foo_service_mock = MockFooServiceTrait::new();
foo_service_mock
.expect_get_result_from_third_party_api()
.return_once(|_| Ok(String::from("some-response-value")));
...
Pro-tip #3: Sequence Matters
If your unit of code must call several dependency methods in a specific order, Mockall allows you to enforce this. This is vital for “unhappy paths” where an early failure in a sequence should prevent subsequent calls, helping you cover edge cases effectively.
// src/controller/foo_controller.rs
...
let mut foo_service_mock = MockFooServiceTrait::new();
foo_service_mock
.expect_get_result_from_third_party_api()
.return_once(|_| Err(()));
...
To run the code, you just need to run cargo run via your terminal or favorite IDE (I personally recommend RustRover). For more details, please, check the README.md file.
Summary of Steps
- Define your dependency as a trait and annotate it with #[automock].
- In the Arrange phase, instantiate the mock and set return values.
- Inject the mock into your service or controller.
- Act by calling the function under test.
- Assert that the result matches your expectations.
The Results
Using Mockall reduces uncertainty by ensuring that failures in a unit test are actually caused by the code being tested, not by a bug in a dependency. This isolation makes tests more performant and repeatable, as they run entirely in memory without external side effects.
Do you need help modernizing your legacy applications, optimizing your architecture or improving your development workflows? Let’s connect on LinkedIn or check out my services on Upwork.
Leave a Reply