精通 Python 设计模式——测试模式

在前几章里,我们介绍了架构模式,以及并发、性能等特定用例的模式。

本章将探讨对测试尤其有用的设计模式 。这些模式有助于隔离组件提高测试可靠性 ,并促进代码复用

本章将涵盖以下主题:

  • Mock Object(模拟对象)模式
  • 依赖注入(Dependency Injection)模式

技术要求

请参见第 1 章中的通用要求。

Mock Object(模拟对象)模式

Mock 对象 通过模拟依赖的行为 来在测试时隔离被测组件,帮助我们构建可控的测试环境验证组件间的交互

Mock 模式提供三项关键能力:

  • 隔离 :将被测单元与其外部依赖隔离开,确保在可预测、无外部副作用的环境中运行测试。
  • 行为验证:可验证在测试过程中是否发生了特定行为,如某方法是否被调用、以何参数被调用、调用顺序等。
  • 简化搭建:用 Mock 替代复杂的真实对象,避免繁琐的环境准备。

与 Stub 的比较

Stub 也会替换真实实现,但主要用于为被测代码提供间接输入 ;而 Mock 除了能提供输入外,还能验证交互,因此在许多测试场景下更灵活。

现实类比

  • 飞行模拟器:在安全可控的环境中复刻飞行,以训练各类场景应对。
  • 心肺复苏(CPR)假人:模拟人体,为训练提供真实且可控的练习环境。
  • 汽车碰撞假人:在不危及人身安全的前提下,收集碰撞数据评估车辆安全性。

典型用法

  • 单元测试:用 Mock 替代复杂、不可靠或不可用的依赖,只关注被测单元本身。例如测试一个从 API 拉取数据的服务时,用 Mock 模拟 API 返回预定义响应,验证服务在各种数据与错误情形下的表现。
  • 集成测试:关注组件间交互。对于尚未开发好或引入成本过高的依赖,用 Mock 代替之,以便测试其他服务与它的集成与通信。
  • 行为验证 :验证对象间的交互是否符合预期。例如在 MVC 中,测试一个控制器是否 调用鉴权、 调用日志记录,然后才处理请求。Mock 可以严格校验调用顺序与参数

实现 Mock Object 模式(Python unittest 示例)

设想我们有一个将消息写入文件的日志函数 。我们可以对文件写入进行 Mock,以验证日志函数是否按预期写入内容,而无需真的创建文件。

导入:

javascript 复制代码
import unittest
from unittest.mock import mock_open, patch

被测类:

python 复制代码
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")

测试用例:

scss 复制代码
class TestLogger(unittest.TestCase):
    def test_log(self):
        msg = "Hello, logging world!"

        m_open = mock_open()
        with patch("builtins.open", m_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")

运行入口:

ini 复制代码
if __name__ == "__main__":
    unittest.main()

关于 builtins

Python 的 builtins 模块提供对所有内建标识符的直接访问;例如内建函数 open() 的全名是 builtins.open
docs.python.org/3/library/b...

关于 mock_open

调用 mock_open() 会返回一个配置成类似内建 open() 行为的 Mock 对象,可模拟读写等文件操作。

关于 unittest.mock.patch
patch() 用于在测试期间用 Mock 临时替换目标对象。其参数包括:

  • target:要替换的对象路径;
  • 可选参数如 new(替换对象)、spec/autospec(限制 Mock 的属性以贴近真实对象)、spec_setside_effect(定义条件行为/异常)、return_valuewraps 等,以便精细控制。

执行命令:

bash 复制代码
python ch10/mock_object.py

可能输出:

markdown 复制代码
.
---------------------------------------------------------
Ran 1 test in 0.012s
OK

这个示例展示了如何在单元测试中使用 Mock 来模拟系统的一部分隔离副作用(如文件 I/O)。这样既不需要真实文件,也无需为测试去改动被测类的结构,同时还能验证其内部行为是否正确。

依赖注入(Dependency Injection, DI)模式

依赖注入模式 指的是:把一个类所需的依赖 作为外部实体 传入,而不是在类内部自行创建。这样可以促进低耦合模块化可测试性

现实示例

  • 家电与电源插座:各种电器都能插到不同插座上取电,而无需永久性的硬连线。
  • 可更换镜头的相机:摄影师根据环境与需求更换镜头,而无需更换机身。
  • 模块化列车系统:根据行程需求增减卧铺车、餐车、行李车等车厢。

适用场景

  • Web 应用中的数据库连接 :将数据库连接对象注入到仓储(repository)或服务(service)中,提高模块化与可维护性;便于在不同数据库引擎/配置间切换而无需修改组件代码。同时,单元测试时可注入模拟(mock)数据库连接,从而在不影响真实库的情况下覆盖各类数据场景与错误处理。
  • 多环境配置管理(开发/测试/生产等) :通过动态注入配置,DI 降低模块与配置来源之间的耦合,便于在不同环境间切换且无需大幅改造。在单元测试中,可以注入特定配置以验证模块在不同配置下的行为,更好地保障健壮性与功能正确性。

实现依赖注入------使用 Mock 对象

第一个示例中,WeatherService 依赖一个 WeatherApiClient 接口来获取天气数据。我们在单元测试里注入该 API 客户端的 mock 版本

定义接口(使用 Python 的 Protocol):

python 复制代码
from typing import Protocol

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

真实实现(示例中简化为返回字符串):

ruby 复制代码
class RealWeatherApiClient:
    def fetch_weather(self, location):
        return f"Real weather data for {location}"

服务类,依赖注入接口实现:

ruby 复制代码
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)

通过构造函数注入依赖并手动测试:

css 复制代码
if __name__ == "__main__":
    ws = WeatherService(RealWeatherApiClient())
    print(ws.get_weather("Paris"))

运行(ch10/dependency_injection/di_with_mock.py):

bash 复制代码
python ch10/dependency_injection/di_with_mock.py

输出:

kotlin 复制代码
Real weather data for Paris

由于单元测试 才是重点,继续添加测试(第二个文件 ch10/dependency_injection/test_di_with_mock.py):

导入与被测类:

javascript 复制代码
import unittest
from di_with_mock import WeatherService

Mock 版本的 API 客户端:

ruby 复制代码
class MockWeatherApiClient:
    def fetch_weather(self, location):
        return f"Mock weather data for {location}"

测试用例:

scss 复制代码
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",
        )

运行测试:

ini 复制代码
if __name__ == "__main__":
    unittest.main()

执行(python ch10/dependency_injection/test_di_with_mock.py):

markdown 复制代码
.
---------------------------------------------------------
Ran 1 test in 0.000s
OK

通过此例可以看到:WeatherService 无需知道 自己使用的是真实 还是模拟 的 API 客户端,系统因而更加模块化,也更容易测试。

实现依赖注入------使用装饰器

也可以通过装饰器 来做 DI,从而简化注入流程。下面构建一个通知系统,可通过不同通道(Email / SMS)发送消息。第一部分演示手动测试结果,第二部分给出单元测试。

定义接口:

python 复制代码
from typing import Protocol

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

两种具体实现:

python 复制代码
class EmailSender:
    def send(self, message: str):
        print(f"Sending Email: {message}")

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

通知服务:

ruby 复制代码
class NotificationService:
    sender: NotificationSender = None
    def notify(self, message):
        self.sender.send(message)

装饰器:为类注入具体的 sender:

python 复制代码
def inject_sender(sender_cls):
    def decorator(cls):
        cls.sender = sender_cls()
        return cls
    return decorator

给服务类加上装饰器(此处注入 EmailSender):

python 复制代码
@inject_sender(EmailSender)
class NotificationService:
    sender: NotificationSender = None
    def notify(self, message):
        self.sender.send(message)

手动测试:

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

运行(ch10/dependency_injection/di_with_decorator.py):

kotlin 复制代码
Sending Email: Hello, this is a test notification!

若把装饰器中的 EmailSender 改为 SMSSender 后重跑:

kotlin 复制代码
Sending SMS: Hello, this is a test notification!

说明 DI 生效。

单元测试(使用 Stub,而非 Mock,以展示另一种方式)

导入:

javascript 复制代码
import unittest
from di_with_decorator import (
    NotificationSender,
    NotificationService,
    inject_sender,
)

Stub 类(记录调用的消息):

ruby 复制代码
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)

测试用例:

ruby 复制代码
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)

运行测试:

ini 复制代码
if __name__ == "__main__":
    unittest.main()

执行(python ch10/dependency_injection/test_di_with_decorator.py):

markdown 复制代码
..
---------------------------------------------------------
Ran 2 tests in 0.000s
OK

由此可见,用装饰器做依赖管理既便于在不改动类内部 的情况下切换实现 ,也能把依赖管理封装在业务逻辑之外。同时,借助 Stub 技术的单元测试可以验证各组件在隔离环境中的行为是否符合预期。

小结

本章我们探讨了两种对整洁代码测试策略 至关重要的模式:Mock Object依赖注入(DI)

  • Mock Object :确保测试隔离 、避免不必要的副作用,同时便于行为验证简化测试准备 。我们演示了如何用 unittest.mock 在单测中模拟组件。
  • 依赖注入(DI) :提供一种灵活、可测试、可维护 的依赖管理框架,不仅适用于测试场景,也适用于通用软件设计。我们先用 Mock 演示了面向单测/集成测的 DI,然后又展示了用装饰器来简化横切的依赖管理。

下一章将稍作转向,讨论 Python 反模式(anti-patterns) :识别常见陷阱并学习如何规避。

相关推荐
数据智能老司机2 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机2 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
未来影子2 小时前
SpringAI(GA):MCP Server 服务鉴权(过滤器版)
架构
poemyang2 小时前
技术圈的“绯闻女孩”:Gossip是如何把八卦秘密传遍全网的?
后端·面试·架构
c8i2 小时前
drf初步梳理
python·django
每日AI新事件2 小时前
python的异步函数
python
使一颗心免于哀伤2 小时前
《设计模式之禅》笔记摘录 - 21.状态模式
笔记·设计模式
这里有鱼汤3 小时前
miniQMT下载历史行情数据太慢怎么办?一招提速10倍!
前端·python
databook12 小时前
Manim实现脉冲闪烁特效
后端·python·动效