Introduction to Mocks in Rust
Introduction to Mocks
As we progress in our understanding of isolating dependencies with test doubles, we've already covered dummies and stubs. In this lesson, we introduce mocks, powerful tools for simulating external dependencies in software tests. In previous lessons, we discussed dummies and stubs, which allow you to put something in place of a dependency. However, mocks allow us to replicate the behavior of complex systems, enabling testing and verification in isolation without dependence on unpredictable systems like databases or web services.
Adhering to the TDD workflow remains crucial:
- Red: Start with a failing test.
- Green: Implement just enough code to pass the test.
- Refactor: Clean up your code without altering its functionality.
Using Rust and the mockall library, we'll demonstrate how mocks are applied to effectively isolate and test application logic.
Why Use Mocks in TDD?
Mocks are essential in TDD, as they allow you to test code independently of the system parts you don't control. When constructing tests for a PricingService, for instance, you might mock an external currency conversion service to avoid failures due to downtime or unexpected changes in the API. Mocks create a controlled environment where various conditions and responses can be simulated, and calls can be validated.
Unlike stubs that merely return data, mocks fully simulate dependencies by preventing the actual code or functionality from executing and providing validation that the mock was called. For example, if a function interacts with an external API, a mock could simulate that API's response without making any network requests and verify the API was called with specific parameters.
Mocking Fundamentals with Mockall
We'll now explore mocking fundamentals using the mockall crate in Rust. We'll initiate by understanding how to create mocks and set up expectations using Rust's capabilities.
Consider the ExchangeRateService, responsible for fetching exchange rates from an API. When testing the PricingService, mocking this service ensures that tests do not depend on real API interactions.
Below is an example using mockall and #[automock] for setting up a test:
In this structure:
- We define an
ExchangeRateServicetrait. MockExchangeRateServiceis created using themockallcrate, allowing us to fake behavior without implementing actual logic.expect_get_rate()sets up the expectation and return value for the method.
Choosing Between #[automock] and mock!
Use #[automock] when you own the trait and can annotate it; it generates a mock with the least setup.
Use mock! when you cannot modify the trait, such as a trait from another crate, or when you prefer to declare the mock directly in a test module.
Both approaches provide the same expectation API, so choose the option that keeps the production interface and test setup clearest.
Hands-On Example: PricingService with Mocks
We now implement the necessary code in PricingService to pass the test:
This code ensures that the test passes by fulfilling the required logic for converting the price using the mocked exchange rate service.
Advanced Mocking Techniques
If you want to mock a result with more than one set of inputs, you can simply configure it for different calls:
A more advanced case:
In Rust, advanced mocking techniques can involve handling more dynamic scenarios, such as matching specific arguments and more complex conditions by using run-time configurations:
This approach allows testing various edge cases and provides flexibility to test alternative paths, ensuring robust real-world readiness. It is significantly more complicated than setting specific expectations for each call, so it should only be used when requiring dynamic responses.
Validating Multiple Calls
When creating multiple mock results, you can easily verify the expectations of multiple calls:
This validates that the mock received the specified number of calls, ensuring the behavior matches expectations during test execution.
Summary and Practice Preparation
This lesson covered the strategic use of mocks within the TDD framework, leveraging Rust and the mockall crate to enhance the reliability and focus of our application's logic:
- Red: Commence with a failing test to clarify the desired functionality.
- Green: Develop the minimal requisite code using mocks to satisfy the test.
- Refactor: Enhance and streamline the code without altering its functionality.
With these concepts at your disposal, you're prepared to continue with the practice exercises, further solidifying your proficiency. By mastering these approaches, you're advancing towards developing more robust, scalable applications in Rust. Keep practicing, and congrats on making it this far in the course!
