Mocking and Patching
Explore mocking and patching techniques with pytest to isolate code from external dependencies and modify function behaviors in tests. Learn to use pytest's monkeypatch fixture and the pytest-mock plugin to create mocks, specify return values, handle side effects, and mock attributes. This lesson helps you write precise tests by simulating parts of your system, improving test reliability and focus.
Introduction to mocking and patching
Mocking and patching are crucial techniques in software testing that allow the replacement of parts of a system with fake objects called mocks. This technique simulates behavior and data, which enables testing specific parts of a system without relying on other parts. As a result, this technique can help isolate and identify issues in the code.
Difference between mocking and patching
Mocking creates a fake object that mimics the behavior of a real object. The purpose is to isolate the code under test from external dependencies, such as databases or web services. By replacing external dependencies with fake objects, the code can be tested in isolation without the need for a real environment. It’s mainly used for unit testing, where a single function or method is tested.
Patching, on the other hand, involves replacing the implementation of a function or method with a mock object. The purpose is to modify the behavior of a function or method that’s called by the code under test. It’s mainly used for integration testing, where the interactions between multiple components of a system are tested.
To sum up, mocking isolates the code under test from external dependencies, while patching modifies the behavior of functions or methods that are called by the code under test.
Patching using monkeypatch
Pytest provides a monkeypatch fixture that helps safely modify attributes, dictionary items, environment variables, or sys.path for importing in tests. It provides methods like setattr(), delattr(), setitem(), delitem(), setenv(), delenv(), syspath_prepend(), and chdir() to modify the behavior of functions, properties of a class, dictionaries, environment variables, and current working directories for a test.
The changes made using monkeypatch are undone after the test. It can be used to patch a function with a desired behavior, modify global configurations, test program behavior with missing environment variables, modify the $PATH variable, and modify sys.path for namespace packages and module imports.
Let’s take a look at an example:
In this example, we define a mock response object with a JSON method that returns the expected response. We then use monkeypatch to replace requests.get with our mock response object so that when get_data is called, it returns our mock response instead of making a real API request.
Mocking using mocker
In this section, we will look into a pytest plugin that allows us to use the mocker fixture.
Plugin: pytest-mock
Pytest offers several mocking and patching libraries, each with unique features and strengths. Even though each library has its own syntax and features, they follow the same basic principles of mocking and patching. Understanding these principles and selecting the right library can make testing more efficient and effective.
The library we will be using for this lesson is pytest-mock. It is a pytest plugin that offers an easier-to-use API and integrates seamlessly with pytest fixtures. When using pytest-mock, mock objects can be created and managed using the built-in mocker fixture. This fixture provides a simplified interface for creating and using mocks within pytest test functions. To create a mock object, simply call the mocker.Mock() method and pass in any necessary parameters. Once the mock object is created, it can be used like any other object in the test function. Assertions can be made against the mock object to ensure that the expected behavior is being simulated.
Code example
For example, consider the following code:
Suppose we have a function called divide that performs a division operation and returns the quotient. We want to test a function called compute that calls divide and performs some additional calculations on the result. However, we don’t want to perform the actual division operation during our test. We can use the mocker fixture to mock the divide function and return a predefined value instead.
In the test_compute test function, we use the mocker.patch method to replace the implementation of module.divide with a mock object that returns the value 2. Now, when we call compute(10, 5) in our test, it uses the mocked version of divide and returns the expected result of 200. Note that we pass the mocker fixture as an argument to the test function to use it.
Return value
This refers to the value that a mocked function or method returns when it is called. For example, we can use mocking to make a function return a specific value instead of the value it would normally return. This can be useful for testing functions that depend on the return value of other functions or methods.
The test uses the mocker fixture to patch the divide() function in the main module. We place the patched function with a fake function that always returns 3. Then, we call the compute() function with arguments 10 and 5, and the return value is checked to be equal to 300, which is the product of 10 and 5 multiplied by the patched value of 3 returned by divide() instead of the correct value 2.
This test verifies the behavior of compute() when the divide() function is replaced with a known value. It checks if the multiplication of the input values and the patched value of divide() is returned correctly.
Side effects
When mocking a function or method, we can specify a callable to be executed instead of the original implementation using the side_effect argument in mocker.patch(). This callable refers to any other changes that the mocked function might make, such as modifying a global variable or writing to a file. By using side_effect, we can replace the real function with a fake function that mimics these side effects and tests the behavior of the code that depends on them. The side_effect argument can also be used to raise an exception when the mock is called.
In the first test, we use mocker.patch() to replace the implementation of my_module.divide() with a lambda function that performs integer division instead of floating-point division. When we call my_module.divide(10, 3) in the test, the lambda function is called instead of the original implementation, and the result is 3 instead of 3.3333.
In the second test, we use mocker.patch() to replace the implementation of my_module.divide() with a ZeroDivisionError exception. When we call my_module.divide(10, 0) in the test, the ZeroDivisionError exception is raised instead of the original implementation, and we use pytest.raises() to assert that the exception is raised.
Attributes
In Python, objects can have attributes, which are like variables attached to the object. In the context of mocking, we might want to mock an object and its attributes so that when the code under test accesses the attribute, it gets the expected value. For example, if we have a class that uses an external service, we might want to mock the service object and its attributes so that the code under test uses the mocked service instead of the real service.
Test your learning!
What is patching in Python testing?
Changing the behavior of a function or class temporarily.
Fixing syntax errors in the code.
Improving the performance of a program.
Automating the process of running tests.