Python > Quality and Best Practices > Testing > Unit Testing with `unittest`

Advanced Unit Testing: Mocking with `unittest.mock`

This snippet demonstrates the use of mocking in unit tests using Python's `unittest.mock` module. Mocking allows you to replace dependencies of the code under test with controlled substitutes, enabling you to isolate and test specific units of code in a predictable environment.

Concepts Behind the Snippet

Mocking is a crucial technique when unit testing code that interacts with external resources, such as databases, APIs, or filesystems. By replacing these dependencies with mock objects, you can avoid real-world side effects and create deterministic test conditions. This allows you to test the logic of your code independently of the behavior of its dependencies.

Example Code

This code defines an `ExternalService` that represents an external dependency. `MyClass` uses this service to process data. The `TestMyClass` uses `@patch` to replace `ExternalService` with a mock object. We configure the mock object to return a specific value when `get_data` is called. This allows us to test the `process_data` method without actually connecting to an external service.

import unittest
from unittest.mock import patch

class ExternalService:
    def get_data(self):
        # Simulates fetching data from an external source
        raise NotImplementedError("Should be implemented")

class MyClass:
    def __init__(self, service):
        self.service = service

    def process_data(self):
        data = self.service.get_data()
        return data.upper()

class TestMyClass(unittest.TestCase):

    @patch('__main__.ExternalService')
    def test_process_data(self, MockExternalService):
        # Configure the mock
        mock_service = MockExternalService.return_value
        mock_service.get_data.return_value = "some data"

        # Instantiate the class with the mock service
        my_instance = MyClass(mock_service)

        # Call the method under test
        result = my_instance.process_data()

        # Assert the result
        self.assertEqual(result, "SOME DATA")

        # Assert that the mock was called
        mock_service.get_data.assert_called_once()

if __name__ == '__main__':
    unittest.main()

Explanation of the Code

  • `ExternalService` and `MyClass`: These classes define the dependency relationship. `MyClass` relies on `ExternalService` to fetch data.
  • `@patch('__main__.ExternalService')`: This decorator replaces the `ExternalService` class with a mock object during the test. The first argument specifies the fully qualified name of the object to be mocked.
  • `MockExternalService`: This is the mock object that replaces `ExternalService`. The `.return_value` attribute allows us to configure the behavior of the mock.
  • `mock_service.get_data.return_value = "some data"`: This configures the mock to return the string 'some data' when the `get_data` method is called.
  • `mock_service.get_data.assert_called_once()`: This asserts that the `get_data` method of the mock object was called exactly once.

Real-Life Use Case

Consider a function that sends an email. In a unit test, you wouldn't want to actually send an email every time the test runs. Instead, you would mock the email sending function to verify that it's called with the correct parameters, without actually sending the email.

Best Practices

  • Mock only external dependencies: Avoid mocking code within the same unit. Focus on mocking interactions with external services, databases, or other components.
  • Configure mocks appropriately: Carefully configure the behavior of your mocks to simulate the expected responses from the real dependencies.
  • Use assertions to verify interactions: Use assertion methods like `assert_called_once`, `assert_called_with`, and `assert_has_calls` to verify that the mock objects were called as expected.
  • Avoid over-mocking: Over-mocking can make your tests brittle and less reliable. Try to strike a balance between isolating dependencies and testing the actual behavior of your code.

Interview Tip

Be prepared to explain the purpose of mocking, the different ways to create mock objects, and how to configure their behavior. You should also be able to discuss the benefits and drawbacks of mocking.

When to Use Them

Use mocking when you need to isolate a unit of code from its dependencies, when you want to avoid real-world side effects, or when you need to create deterministic test conditions.

Memory Footprint

The memory footprint of mock objects is generally small. However, if you are creating a large number of mocks or if your mock objects contain large data structures, it's important to be mindful of memory usage.

Alternatives

Other mocking libraries available in Python include `mock` (the older version of `unittest.mock`, now deprecated), `doublex`, and others. However, `unittest.mock` is the standard and generally preferred option.

Pros

  • Isolate units of code: Allows you to test individual components in isolation.
  • Avoid real-world side effects: Prevents tests from modifying external resources.
  • Create deterministic test conditions: Enables you to control the behavior of dependencies.

Cons

  • Can make tests brittle: Over-mocking can lead to tests that are too closely tied to the implementation details.
  • Requires careful configuration: Mocks need to be carefully configured to simulate the expected behavior of dependencies.
  • Can be complex: Mocking can be a complex topic, especially when dealing with complex dependencies.

FAQ

  • What is the difference between `patch` and `Mock`?

    `patch` is a decorator or context manager that replaces an object with a mock object for the duration of the test. `Mock` is a class that creates a mock object. `patch` often uses `Mock` internally.
  • How do I verify that a mock was called with specific arguments?

    Use the `assert_called_with` method to verify that the mock was called with specific arguments. For example: `mock_service.get_data.assert_called_with(arg1, arg2)`.
  • Can I mock multiple objects in a single test?

    Yes, you can use multiple `@patch` decorators or context managers to mock multiple objects in a single test.