在前几章里,我们介绍了架构模式,以及并发、性能等特定用例的模式。
本章将探讨对测试尤其有用的设计模式 。这些模式有助于隔离组件 、提高测试可靠性 ,并促进代码复用。
本章将涵盖以下主题:
- 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_set
、side_effect
(定义条件行为/异常)、return_value
、wraps
等,以便精细控制。
执行命令:
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) :识别常见陷阱并学习如何规避。