Mocking Dependencies with Pytest-Mock in TDD
Introduction to Mocks
In our journey through isolating dependencies with test doubles, we've explored dummies, stubs, and spies. This lesson focuses on mocks, a powerful type of test double used in Python to simulate external dependencies in software tests. You may have noticed in the previous unit how spying on the real implementation can become cumbersome. Mocks can imitate the behavior of complex systems, allowing us to test code in isolation without relying on real, unpredictable systems like databases or web services.
Now, let's review the TDD workflow:
- Red: Write a failing test.
- Green: Write the minimum code to pass the test.
- Refactor: Improve the code structure without changing its behavior.
We'll demonstrate these principles using mocks, helping you to effectively isolate and test your application logic.
Why Use Mocks in TDD?
Mocks are indispensable in TDD because they allow you to test your code independently of the parts of the system you don't control. For example, when writing tests for a PricingService, you don't want the tests to fail due to external issues like a currency conversion API being down. Mocks provide a controlled environment where you can simulate various conditions and responses, as well as validate the calls.
Mocks differ from spies in that they fully simulate dependencies rather than just observing behavior. A mock creates a controlled substitute for a dependency, ensuring that actual code or functionality isn’t executed. For instance, if a function interacts with an external API, a mock can simulate different responses from that API without making a network request.
Mocking Fundamentals with pytest-mock
Let's dive into mocking with pytest-mock. We'll start with the basics: how to mock a class and its methods in Python.
Consider the ExchangeRateService, which fetches exchange rates from an API. In testing the PricingService, we need to mock this service to ensure our tests don't rely on actual API responses.
Here's a simple way to mock with pytest-mock:
In this setup:
- We use
mocker.Mockto replace theExchangeRateServicewith a mock version. mock_exchange_rate_service.get_rate.return_valueis used to control the return value of the method we're simulating.
When creating a mock with mocker.Mock(), we're creating a stand-in object that simulates the ExchangeRateService. This mock object provides methods that don't contain any real logic but allow us to control their returned values and behavior. By injecting this mock into our PricingService, we ensure that tests are decoupled from external systems, enabling focused testing of our service's logic without dependencies on actual API calls.
