ZhangZhihui's Blog  

• The Mock Object pattern

The Mock Object pattern provides three features:

1.Isolation: Mocks isolate the unit of code being tested, ensuring that tests run in a controlled environment where dependencies are predictable and do not have external side effects.
2.Behavior verification: By using mock objects, you can verify that certain behaviors happen during a test, such as method calls or property accesses.
3.Simplification: They simplify the setup of tests by replacing complex real objects that might require significant setup of their own.

Implementing the Mock Object pattern

Imagine we have a function that logs messages to a file. We can mock the file-writing mechanism to ensure our logging function writes the expected content to the log without writing to a file. Let’s see how this can be implemented using Python’s unittest module.

import unittest
from unittest.mock import mock_open, patch


class Logger:
    def __init__(self, filepath):
        self.filepath = filepath

    def log(self, message):
        with open(self.filepath, "a") as file:
            file.write(f"{message}\n")


class TestLogger(unittest.TestCase):
    def test_log(self):
        msg = "Hello, logging world!"

        m_open = mock_open()

        with patch("builtins.open", mock_open):
            logger = Logger("dummy.log")
            logger.log(msg)

            m_open.assert_called_once_with("dummy.log", "a")
            m_open().write.assert_called_once_with(f"{msg}\n")


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

We create a class representing a simple logger that writes messages to a file specified during initialization.

Next, we create a test case class that inherits from the unittest.TestCase class, as usual. In this class, we need the test_log() method to test the logger’s log() method.

Next, we mock the Python built-in open() function directly within the test scope. Mocking the function is done using unittest.mock.patch(), which temporarily replaces the target object, builtins.open, with a mock object (the result of calling mock_open()). With the context manager we get from calling the unittest.mock.patch() function, we create a Logger object and call its .log() method, which should trigger the open() function.

About mock_open
When you call mock_open(), it returns a Mock object that is configured to behave like the built-in open() function. This mock is set up to simulate file operations such as reading and writing.

About unittest.mock.patch
It is used to replace objects with mocks during testing. Its arguments include target to specify the object to replace, and optional arguments: new for an optional replacement object, spec and autospec to limit the mock to the real object’s attributes for accuracy, spec_set for a stricter attribute specification, side_effect to define conditional behavior or exceptions, return_value for setting a fixed response, and wraps to allow the original object’s behavior while modifying certain aspects. These options enable precise control and flexibility in testing scenarios.

We check that the log file was opened correctly, which we do using two verifications. For the first one, we use the assert_called_once_with() method on the mock object, to check that the open() function was called with the expected parameters. For the second one, we need more tricks from unittest.mock.mock_open; our m_open mock object, which was obtained by calling the mock_open() function, is also a callable object that behaves like a factory for creating new mock file handles each time it’s called. We use that to obtain a new file handle, and then we use assert_called_once_with() on the write() method call on that file handle, which helps us check if the write() method was called with the correct message.

• The Dependency Injection pattern

The Dependency Injection pattern involves passing the dependencies of a class as external entities rather than creating them within the class. This promotes loose coupling, modularity, and testability.

Real-world examples

Electrical appliances and power outlets: Various electrical appliances can be plugged into different power outlets to use electricity without needing direct and permanent wiring
Lenses on cameras: A photographer can change lenses on a camera to suit different environments and needs without changing the camera itself
Modular train systems: In a modular train system, individual cars (such as sleeper, diner, or baggage cars) can be added or removed depending on the needs of each journey

Use cases for the Dependency Injection pattern

In web applications, injecting database connection objects into components such as repositories or services enhances modularity and maintainability. This practice allows for an easy switch between different database engines or configurations without the need to alter the component’s code directly.

It also significantly simplifies the process of unit testing by enabling the injection of mock database connections, thereby testing various data scenarios without affecting the live databases. Another type of use case is managing configuration settings across various environments (development, testing, production, and so on). By dynamically injecting settings into modules, dependency injection (DI) reduces coupling between the modules and their configuration sources. This flexibility makes it easier to manage and switch environments without needing extensive reconfiguration. In unit testing, this means you can inject specific settings to test how modules perform under different configurations, ensuring robustness and functionality.

Implementing the Dependency Injection pattern – using a mock object

In this first example, we’ll create a simple scenario where a WeatherService class depends on a WeatherApiClient interface to fetch weather data. For the example’s unit test code, we will inject a mock version of this API client.

We start by defining the interface any weather API client implementation should conform to, using Python’s Protocol feature:

from typing import Protocol


class WeatherApiClient(Protocol):
    def fetch_weather(self, location):
        """Fetch weather data for a given location"""
        ...

Then, we add a RealWeatherApiClient class that implements that interface and that would interact with our weather service. In a real scenario, in the provided fetch_weather() method, we would perform a call to a weather service, but we want to keep the example simple and focus on the main concepts of this chapter; so, we provide a simulation, simply returning a string that represents the weather data result. The code is as follows:

class RealWeatherApiClient:
    def fetch_weather(self, location):
        return f"Real weather data for {location}"

Next, we create a weather service, which uses an object that implements the WeatherApiClient interface to fetch weather data:

class WeatherService:
    def __init__(self, weather_api: WeatherApiClient):
        self.weather_api = weather_api

    def get_weather(self, location):
        return self.weather_api.fetch_weather(location)

Finally, we are ready to inject the API client’s dependency through the WeatherService constructor. We add code that helps manually test the example, using the real service, as follows:

if __name__ == "__main__":
    ws = WeatherService(RealWeatherApiClient())
    print(ws.get_weather("Paris"))

You should get the following output:

Real weather data for Paris

 

First, we import the unittest module, as well as the WeatherService class (from our di_with_mock module), as follows:

import unittest
from di_with_mock import WeatherService

Then, we create a mock version of the weather API client implementation that will be useful for unit testing, simulating responses without making real API calls:

class MockWeatherApiClient:
    def fetch_weather(self, location):
        return f"Mock weather data for {location}"

Next, we write the test case class, with a test function. In that function, we inject the mock API client instead of the real API client, passing it to the WeatherService constructor, as follows:

class TestWeatherService(unittest.TestCase):
    def test_get_weather(self):
        mock_api = MockWeatherApiClient()
        weather_service = WeatherService(mock_api)
        self.assertEqual(
            weather_service.get_weather("Anywhere"),
            "Mock weather data for Anywhere",
        )

We finish by adding the usual lines for executing unit tests when the file is interpreted by Python:

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

Through this example, we were able to see that the WeatherService class doesn’t need to know whether it’s using a real or a mock API client, making the system more modular and easier to test.

Implementing the Dependency Injection pattern – using a decorator

It is also possible to use decorators for DI, which simplifies the injection process. Let’s see a simple example demonstrating how to do that, where we’ll create a notification system that can send notifications through different channels (for example, email or SMS). The first part of the example will show the result based on manual testing, and the second part will provide unit tests.

First, we define a NotificationSender interface, outlining the methods any notification sender should have:

from typing import Protocol


class NotificationSender(Protocol):
    def send(self, message: str):
        """Send a notification with the given message"""
        ...

Then, we implement two specific notification senders: the EmailSender class implements sending a notification using email, and the SMSSender class implements sending using SMS. This part of the code is as follows:

class EmailSender:
    def send(self, message: str):
        print(f"Sending Email: {message}")


class SMSSender:
    def send(self, message: str):
        print(f"Sending SMS: {message}")

We also define a notification service class, NotificationService, with a class attribute sender and a .notify() method, which takes in a message and calls .send() on the provided sender object to send the message, as follows:

class NotificationService:
    sender: NotificationSender = None

    def notify(self, message):
        self.sender.send(message)

What is missing is the decorator that will operate the DI, to provide the specific sender object to be used. We create our decorator to decorate the NotificationService class for injecting the sender. It will be used by calling @inject_sender(EmailSender) if we want to inject the email sender, or @inject_sender(SMSSender) if we want to inject the SMS sender. The code for the decorator is as follows:

def inject_sender(sender_cls):
    def decorator(cls):
        cls.sender = sender_cls()
        return cls
    return decorator

Now, if we come back to the notification service’s class, the code would be as follows:

@inject_sender(EmailSender)
class NotificationService:
    sender: NotificationSender = None

    def notify(self, message):
        self.sender.send(message)

Finally, we can instantiate the NotificationService class in our application and notify a message for testing the implementation, as follows:

if __name__ == "__main__":
    service = NotificationService()
    service.notify("Hello, this is a test notification!")

 

Next, we want to write unit tests for that implementation. We could use the mocking technique, but to see other ways, we are going to use the stub classes approach. The stubs manually implement the dependency interfaces and include additional mechanisms to verify that methods have been called correctly. Let’s start by importing what we need:

import unittest

from di_with_decorator import (
    NotificationSender,
    NotificationService,
    inject_sender,
)

Then, we create stub classes that implement the NotificationSender interface. These classes will help record calls to their send() method, using the messages_sent attribute on their instances, allowing us to check whether the correct methods were called during the test. Both stub classes are as follows:

class EmailSenderStub:
    def __init__(self):
        self.messages_sent = []

    def send(self, message: str):
        self.messages_sent.append(message)


class SMSSenderStub:
    def __init__(self):
        self.messages_sent = []

    def send(self, message: str):
        self.messages_sent.append(message)

Next, we are going to use both stubs in our test case to verify the functionality of NotificationService. In the test function, test_notify_with_email, we create an instance of EmailSenderStub, inject that stub into the service, send a notification message, and then verify that the message was sent by the email stub. That part of the code is as follows:

class TestNotifService(unittest.TestCase):
    def test_notify_with_email(self):
        email_stub = EmailSenderStub()

        service = NotificationService()
        service.sender = email_stub
        service.notify("Test Email Message")

        self.assertIn(
            "Test Email Message",
            email_stub.messages_sent,
        )

    def test_notify_with_sms(self):
        sms_stub = SMSSenderStub()

        @inject_sender(SMSSenderStub)
        class CustomNotificationService:
            sender: NotificationSender = None

            def notify(self, message):
                self.sender.send(message)

        service = CustomNotificationService()
        service.sender = sms_stub
        service.notify("Test SMS Message")

        self.assertIn(
            "Test SMS Message", sms_stub.messages_sent
        )

We need another function for the notification with SMS functionality, test_notify_with_sms. Similarly to the previous case, we create an instance of SMSSenderStub. Then, we need to inject that stub into the notification service. But, for that, in the scope of the test, we define a custom notification service class, and decorate it with @inject_sender(SMSSenderStub).

Based on that, we inject the SMS sender stub into the custom service, send a notification message, and then verify that the message was sent by the SMS stub. 

Finally, we should not forget to add the lines needed for executing unit tests when the file is interpreted by Python:

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

So, this example showed how using a decorator to manage dependencies allows for easy changes without modifying the class internals, which not only keeps the application flexible but also encapsulates the dependency management outside of the core business logic of your application. In addition, we saw how DI can be tested with unit tests using the stubs technique, ensuring the application’s components work as expected in isolation.

posted on 2024-08-26 21:03  ZhangZhihuiAAA  阅读(6)  评论(0编辑  收藏  举报