Python 单元测试与 Mock 体系全解
Python 单元测试与 Mock 体系全解:从 unittest 到 pytest 的主流实践
1. 为什么要写测试?
作为 Java 开发者,你一定熟悉 JUnit + Mockito 这对黄金组合。在 Python 世界里,也有一套完整且成熟的测试生态。区别在于:Python 的测试工具更灵活、更"Pythonic",上手成本低但功能强大。
本文将从 Python 标准库的unittest出发,逐步介绍unittest.mock、pytest、pytest-mock等主流方案,并穿插 Java 对比帮助你快速建立知识映射。
2. 测试体系全景图
| Python 工具 | 定位 | Java 对应 |
|---|---|---|
unittest | 标准库测试框架 | JUnit |
unittest.mock | 标准库 Mock 工具 | Mockito |
pytest | 第三方测试框架(业界主流) | JUnit 5 + AssertJ |
pytest-mock | pytest 的 mock 插件 | Mockito(注解注入风格) |
pytest-cov | 覆盖率统计 | JaCoCo |
hypothesis | 基于属性的测试 | jqwik |
factory_boy | 测试数据工厂 | EasyRandom / TestContainers |
responses/httpx_mock | HTTP Mock | WireMock |
3. unittest:标准库的基石
3.1 基本结构
importunittestclassCalculator:defadd(self,a:int,b:int)->int:returna+bdefdivide(self,a:int,b:int)->float:ifb==0:raiseValueError("除数不能为零")returna/bclassTestCalculator(unittest.TestCase):"""类似 JUnit 的 @Test 注解,这里用方法名 test_ 前缀标识测试方法"""defsetUp(self):"""每个测试方法执行前调用,类似 JUnit 的 @BeforeEach"""self.calc=Calculator()deftearDown(self):"""每个测试方法执行后调用,类似 JUnit 的 @AfterEach"""passdeftest_add(self):self.assertEqual(self.calc.add(1,2),3)deftest_divide(self):self.assertAlmostEqual(self.calc.divide(10,3),3.333,places=3)deftest_divide_by_zero(self):withself.assertRaises(ValueError)asctx:self.calc.divide(1,0)self.assertIn("除数不能为零",str(ctx.exception))if__name__=="__main__":unittest.main()3.2 生命周期方法对照
| Python (unittest) | Java (JUnit 5) | 说明 |
|---|---|---|
setUp | @BeforeEach | 每个测试前执行 |
tearDown | @AfterEach | 每个测试后执行 |
setUpClass | @BeforeAll | 整个类只执行一次 |
tearDownClass | @AfterAll | 整个类只执行一次 |
3.3 常用断言
self.assertEqual(a,b)# assertEqualsself.assertTrue(x)# assertTrueself.assertIsNone(x)# assertNullself.assertIn(item,container)# assertTrue(list.contains(item))self.assertRaises(Exception)# assertThrowsself.assertAlmostEqual(a,b)# 浮点近似比较(JUnit 中需手动指定 delta)4. unittest.mock:Python 内置的 Mock 利器
从 Python 3.3 开始,unittest.mock被纳入标准库。它提供了Mock、MagicMock、patch三个核心工具。
4.1 Mock 与 MagicMock
fromunittest.mockimportMock,MagicMock# Mock:基础 mock 对象,访问任意属性/方法都会返回新的 Mockm=Mock()m.some_method(1,2,3)m.some_method.assert_called_once_with(1,2,3)# 验证调用# MagicMock:继承 Mock,额外支持魔术方法(__len__、__iter__ 等)mm=MagicMock()mm.__len__.return_value=5len(mm)# 返回 5类比 Java:Mock类似 Mockito 的mock(SomeClass.class),所有方法默认返回 null/0/false。Python 的 Mock 更激进——访问任何属性都不会报错,直接返回一个新 Mock 对象。
4.2 配置返回值与副作用
fromunittest.mockimportMock# 设置返回值api_client=Mock()api_client.get_user.return_value={"id":1,"name":"Alice"}# 设置多次不同返回值api_client.get_user.side_effect=[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},]# 设置抛出异常api_client.get_user.side_effect=ConnectionError("网络超时")# 设置为可调用函数(动态返回)api_client.get_user.side_effect=lambdauser_id:{"id":user_id,"name":f"User_{user_id}"}4.3 patch:替换目标对象(核心能力)
patch是 Python Mock 体系中最重要也最容易踩坑的功能。它用于在测试期间临时替换指定模块中的对象。
# services.pyimportrequestsdefget_weather(city:str)->str:response=requests.get(f"https://api.weather.com/{city}")data=response.json()returnf"{city}:{data['temp']}°C"# test_services.pyfromunittest.mockimportpatch,Mockfromservicesimportget_weatherclassTestGetWeather:@patch("services.requests.get")# 注意:patch 的是 services 模块中的 requests.getdeftest_get_weather(self,mock_get):# 配置 mock 响应mock_response=Mock()mock_response.json.return_value=