ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python单元测试框架unittest详解:从核心概念到Mock实战

Python单元测试框架unittest详解:从核心概念到Mock实战 1. 为什么我们需要一个测试框架如果你写过Python代码尤其是稍微复杂一点的脚本或者项目大概率经历过这样的场景你修改了一个函数然后手动运行几个例子看看输出对不对。改了几次之后你开始不确定之前的功能是否还正常于是又得把那些例子重新跑一遍。更糟的是当别人接手你的代码或者你需要重构一个模块时这种“手动测试”的方式不仅效率低下而且极易出错因为你很难记住所有需要验证的边界情况。代码的可靠性在这种模式下完全依赖于开发者的记忆力和责任心这显然是不可持续的。这时候一个结构化的、自动化的测试框架就显得至关重要了。它能把你的测试用例Test Cases组织起来定义好预期的输入和输出然后一键运行所有测试。每次代码变更后跑一遍测试集就能快速知道新改动是否破坏了原有的功能。这就像是给你的代码库建立了一套自动化的“免疫系统”。在Python的世界里unittest就是这样一个内置于标准库的“元老级”测试框架。它借鉴了Java领域著名的JUnit框架的设计思想提供了一套完整的测试解决方案。虽然现在有像pytest这样更灵活、更强大的后起之秀但unittest的优势在于“开箱即用”——无需安装任何第三方包并且其基于类的设计对于有面向对象编程背景的开发者来说非常直观。理解unittest不仅是掌握一种工具更是理解单元测试的核心概念和最佳实践。2. unittest的核心四要素TestCase, TestSuite, TestRunner, TestFixture要玩转unittest首先得理解它的四个核心组成部分。你可以把它们想象成一个测试流水线上的不同工位。2.1 TestCase测试用例的载体TestCase测试用例是unittest的基石。每一个独立的测试场景比如“测试用户登录功能”都会被封装在一个继承自unittest.TestCase的类中。在这个类里每一个以test_开头的方法都会被框架自动识别为一个具体的测试点。import unittest class TestMathOperations(unittest.TestCase): def test_addition(self): # 测试加法1 1 应该等于 2 self.assertEqual(1 1, 2) def test_subtraction(self): # 测试减法5 - 3 应该等于 2 self.assertEqual(5 - 3, 2)这里TestMathOperations就是一个测试用例类它包含了两个测试方法test_addition和test_subtraction。self.assertEqual是TestCase提供的众多“断言方法”之一用于判断实际结果是否等于期望结果。如果相等测试通过否则测试失败。注意测试方法名必须以test_开头这是unittest发现测试方法的默认规则。你可以通过重写来修改这个规则但绝大多数情况下遵循它是最佳实践。2.2 TestSuite测试用例的集装箱当你的项目有几十上百个测试用例类时你可能不想一次性运行所有测试或者希望按照特定顺序来运行它们。TestSuite测试套件就是用来组织和打包多个TestCase或TestSuite的容器。import unittest from test_math import TestMathOperations from test_string import TestStringMethods # 创建一个测试套件 suite unittest.TestSuite() # 添加整个测试类 suite.addTest(unittest.makeSuite(TestMathOperations)) # 添加单个测试方法 suite.addTest(TestStringMethods(test_upper)) # 也可以从模块加载 loader unittest.TestLoader() suite_from_module loader.loadTestsFromModule(test_math)通过组合不同的TestSuite你可以创建针对模块、功能或集成场景的定制化测试集。2.3 TestRunner测试的执行者TestRunner测试运行器负责执行测试并输出结果。最常用的是unittest.TextTestRunner它会将结果以文本形式输出到控制台。if __name__ __main__: # 创建一个运行器verbosity2表示输出详细信息 runner unittest.TextTestRunner(verbosity2) # 运行我们之前创建的套件 runner.run(suite)更常见的简便写法是直接使用unittest.main()它会自动发现当前模块中所有TestCase子类并运行它们。if __name__ __main__: unittest.main()2.4 TestFixture测试环境的搭建与清理TestFixture测试夹具不是一个具体的类而是一个概念指的是运行测试所需的前置准备setUp和后置清理tearDown工作。比如测试数据库操作前需要连接数据库并插入测试数据测试结束后需要断开连接并清空测试数据。这些工作可以通过重写TestCase类中的setUp和tearDown方法来实现。setUp: 在每个测试方法开始前自动调用。tearDown: 在每个测试方法结束后自动调用无论测试成功还是失败。setUpClass: 在整个测试类开始前调用一次需使用classmethod装饰器。tearDownClass: 在整个测试类结束后调用一次需使用classmethod装饰器。import unittest import sqlite3 import os class TestUserDatabase(unittest.TestCase): classmethod def setUpClass(cls): 类级别夹具创建测试数据库和表 cls.test_db test_users.db cls.conn sqlite3.connect(cls.test_db) cls.cursor cls.conn.cursor() cls.cursor.execute(CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)) cls.conn.commit() print(Set up test database.) classmethod def tearDownClass(cls): 类级别夹具删除测试数据库文件 cls.conn.close() if os.path.exists(cls.test_db): os.remove(cls.test_db) print(Torn down test database.) def setUp(self): 方法级别夹具每个测试前插入一条公共数据 self.cursor.execute(INSERT INTO users (name) VALUES (fixture_user)) self.conn.commit() def tearDown(self): 方法级别夹具每个测试后清空users表 self.cursor.execute(DELETE FROM users) self.conn.commit() def test_insert_user(self): self.cursor.execute(INSERT INTO users (name) VALUES (alice)) self.conn.commit() self.cursor.execute(SELECT name FROM users WHERE namealice) result self.cursor.fetchone() self.assertIsNotNone(result) # 断言查询结果不为None self.assertEqual(result[0], alice) # 断言名字是alice def test_user_count(self): # setUp方法已经插入了一条‘fixture_user’ self.cursor.execute(SELECT COUNT(*) FROM users) count self.cursor.fetchone()[0] self.assertEqual(count, 1)在这个例子中setUpClass和tearDownClass负责整个测试类生命周期内的数据库创建和销毁而setUp和tearDown则为每个独立的测试方法准备和清理数据。注意test_user_count方法它验证的是setUp中插入的那条数据这体现了夹具的作用。实操心得合理使用夹具能极大提升测试代码的整洁度和可维护性。但要注意setUp中准备的数据应该是测试的“初始状态”避免在一个测试方法中修改了数据而影响到另一个测试方法除非你明确要测试这种依赖。unittest默认会为每个测试方法创建一个新的测试用例实例但像数据库连接这种重型资源放在setUpClass中共享是更高效的做法。3. 断言测试逻辑的裁判官断言是测试的灵魂它定义了什么是“正确”。unittest.TestCase提供了丰富的断言方法用于验证各种条件。3.1 基础断言方法以下是一些最常用的断言方法assertEqual(a, b, msgNone): 断言a bassertNotEqual(a, b, msgNone): 断言a ! bassertTrue(x, msgNone): 断言x为 TrueassertFalse(x, msgNone): 断言x为 FalseassertIs(a, b, msgNone): 断言a is bassertIsNot(a, b, msgNone): 断言a is not bassertIsNone(x, msgNone): 断言x is NoneassertIsNotNone(x, msgNone): 断言x is not NoneassertIn(a, b, msgNone): 断言a in bassertNotIn(a, b, msgNone): 断言a not in bassertIsInstance(obj, cls, msgNone): 断言obj是cls的实例assertNotIsInstance(obj, cls, msgNone): 断言obj不是cls的实例assertRaises(exc, callable, *args, **kwargs): 断言调用callable(*args, **kwargs)会引发exc异常assertRaisesRegex(exc, r, callable, *args, **kwargs): 断言引发异常且异常信息匹配正则表达式rassertWarns(warn, callable, *args, **kwargs): 断言调用会触发指定警告assertAlmostEqual(a, b, places7, msgNone, deltaNone): 断言a和b在指定精度 (places或delta) 内近似相等用于浮点数比较assertNotAlmostEqual(a, b, places7, msgNone, deltaNone): 断言不近似相等assertCountEqual(a, b, msgNone): 断言序列a和b包含相同的元素且元素数量相同不考虑顺序比较[1, 2, 2]和[2, 1, 2]会通过但和[1, 2]不通过assertListEqual(a, b, msgNone),assertTupleEqual,assertSetEqual,assertDictEqual: 针对特定容器类型的相等断言失败时会给出更详细的差异信息。assertGreater(a, b, msgNone),assertGreaterEqual,assertLess,assertLessEqual: 大小比较断言。assertRegex(s, r, msgNone): 断言字符串s匹配正则表达式rassertNotRegex(s, r, msgNone): 断言字符串s不匹配正则表达式r3.2 使用断言的最佳实践选择最精确的断言不要只用assertTrue。assertEqual、assertIn、assertIsNone等能更清晰地表达测试意图并且在失败时能提供更有用的错误信息比如会打印出a和b的实际值。善用msg参数当断言失败时默认信息可能不够清晰。使用msg参数可以提供自定义的失败信息帮助快速定位问题。self.assertEqual(user.role, admin, msgfUser {user.id} should be admin but got {user.role})浮点数比较要用assertAlmostEqual由于浮点数的精度问题直接使用assertEqual(0.1 0.2, 0.3)很可能会失败。应该使用assertAlmostEqual(0.1 0.2, 0.3)或指定delta参数。异常断言测试函数是否在特定条件下抛出预期异常是验证错误处理逻辑的关键。def test_divide_by_zero(self): # 断言调用 math_utils.divide(10, 0) 会抛出 ZeroDivisionError with self.assertRaises(ZeroDivisionError): math_utils.divide(10, 0) # 也可以断言异常信息中包含特定文本 with self.assertRaisesRegex(ValueError, invalid literal): int(not_a_number)3.3 断言失败与错误在unittest的输出中你会看到F(Failure)、E(Error) 和.(Success) 等标识。失败Failure断言没有通过。这意味着代码的行为与预期不符是功能性的错误。错误Error测试代码本身在执行过程中抛出了未捕获的异常比如setUp中出错、测试方法里调用的代码有Bug等。这通常意味着测试环境或测试逻辑有问题。理解这两者的区别有助于快速诊断问题看到F就去检查业务逻辑看到E就去检查测试代码和环境。4. 组织与发现让测试井井有条随着项目增长测试文件会越来越多。良好的组织结构是可持续测试的基础。4.1 测试文件与目录结构一个常见的Python项目测试目录结构如下my_project/ ├── src/ # 源代码目录 │ ├── __init__.py │ ├── calculator.py │ └── utils.py ├── tests/ # 测试代码目录 │ ├── __init__.py # 让tests成为一个包 │ ├── unit/ # 单元测试 │ │ ├── __init__.py │ │ ├── test_calculator.py │ │ └── test_utils.py │ └── integration/ # 集成测试 │ ├── __init__.py │ └── test_api.py ├── requirements.txt └── setup.py关键点测试目录tests通常与源代码目录src平行。在tests目录及其子目录下放置__init__.py文件使其成为一个Python包这样测试发现和导入会更顺畅。测试文件以test_开头如test_calculator.py这样unittest的发现机制和像pytest这样的工具都能自动识别。测试类继承unittest.TestCase测试方法以test_开头。4.2 测试发现与运行unittest提供了强大的测试发现功能。你可以通过命令行或代码来运行测试。1. 命令行运行最常用在项目根目录my_project/下你可以使用以下命令# 1. 发现并运行所有测试 python -m unittest discover # 2. 指定从tests目录开始发现 python -m unittest discover -s tests # 3. 指定发现模式默认是test*.py python -m unittest discover -s tests -p *test*.py # 4. 运行一个特定的测试模块 python -m unittest tests.unit.test_calculator # 5. 运行一个测试类 python -m unittest tests.unit.test_calculator.TestCalculator # 6. 运行一个单独的测试方法 python -m unittest tests.unit.test_calculator.TestCalculator.test_addition # 7. 增加输出详细程度 python -m unittest discover -vdiscover命令会递归查找当前目录及其子目录下所有符合命名规则的测试文件test*.py并执行其中的测试。2. 在代码中运行你也可以在测试文件的末尾使用unittest.main()然后直接运行该文件。# test_calculator.py import unittest class TestCalculator(unittest.TestCase): # ... 测试方法 ... if __name__ __main__: unittest.main(verbosity2) # verbosity2 输出详细信息然后执行python test_calculator.py。4.3 跳过测试与预期失败有时某些测试需要暂时跳过比如依赖的外部服务不可用或者功能尚未实现。unittest提供了装饰器来处理这种情况。unittest.skip(reason): 无条件跳过该测试。unittest.skipIf(condition, reason): 如果条件为真则跳过。unittest.skipUnless(condition, reason): 除非条件为真否则跳过。unittest.expectedFailure: 标记该测试预期会失败。如果测试失败了结果会被记为“预期失败”如果意外通过了则会被记为“意外成功”。这常用于标记已知的Bug。import unittest import sys class TestPlatformSpecific(unittest.TestCase): unittest.skip(这个功能还没实现先跳过) def test_future_feature(self): self.fail(还没实现呢) unittest.skipIf(sys.platform ! win32, 仅在Windows上运行) def test_windows_only(self): # 测试一些Windows特有的逻辑 pass unittest.skipUnless(hasattr(os, symlink), 需要支持符号链接) def test_symlink(self): # 测试符号链接相关功能 pass unittest.expectedFailure def test_buggy_feature(self): # 这是一个已知的Bug我们预期它会失败 self.assertEqual(1, 2) # 这行会失败但被标记为“预期失败”在测试输出中跳过的测试会显示s预期失败的测试如果失败会显示x如果意外通过则会显示u。实操心得合理使用跳过装饰器可以让测试报告更清晰避免因为环境问题或未完成功能导致大量“噪音”失败。但切忌滥用长期被跳过的测试很容易被遗忘最终变成“僵尸测试”。对于已知Bug使用expectedFailure比直接跳过更好因为它能提醒我们当Bug修复后这个测试应该通过。5. 模拟Mocking隔离测试的艺术单元测试的核心思想是“隔离”。我们只想测试当前单元如一个函数、一个类的逻辑而不希望受到数据库、网络、文件系统或其他外部服务的影响。unittest.mock模块Python 3.3 内置就是用来创建“替身”Mock对象模拟这些外部依赖行为的强大工具。5.1 Mock和MagicMockMock和MagicMock是unittest.mock的核心类。MagicMock是Mock的子类它默认实现了大部分魔术方法如__len__,__iter__等用起来更方便。from unittest.mock import Mock, MagicMock # 创建一个Mock对象 mock_obj Mock() # 访问一个不存在的属性会得到一个新的Mock对象 print(mock_obj.some_attribute) # Mock namemock.some_attribute id... # 调用一个不存的方法也会返回一个Mock对象 print(mock_obj.some_method()) # Mock namemock.some_method() id... # 你可以预先指定返回值 mock_obj.calculate.return_value 42 print(mock_obj.calculate()) # 42 # MagicMock 行为类似但支持魔术方法 magic_mock MagicMock() len(magic_mock) # 默认返回0因为MagicMock实现了__len__ magic_mock.__len__.return_value 10 len(magic_mock) # 105.2 打补丁patching最常用的模拟技术是“打补丁”patching即临时替换掉代码中某个对象。unittest.mock提供了patch装饰器和上下文管理器。场景假设我们有一个函数get_user_from_api它内部调用了requests.get来访问网络。我们测试时不想真的发起网络请求。# src/user_service.py import requests def get_user_from_api(user_id): response requests.get(fhttps://api.example.com/users/{user_id}) if response.status_code 200: return response.json() return None# tests/unit/test_user_service.py import unittest from unittest.mock import patch, Mock from src.user_service import get_user_from_api class TestUserService(unittest.TestCase): # 使用装饰器打补丁将‘src.user_service.requests.get’替换为一个Mock对象 patch(src.user_service.requests.get) def test_get_user_success(self, mock_get): # 1. 配置Mock对象的行为 mock_response Mock() mock_response.status_code 200 mock_response.json.return_value {id: 1, name: Alice} mock_get.return_value mock_response # 2. 执行被测试函数 result get_user_from_api(1) # 3. 断言函数返回了预期结果 self.assertEqual(result, {id: 1, name: Alice}) # 4. 断言requests.get被以正确的参数调用了一次 mock_get.assert_called_once_with(https://api.example.com/users/1) patch(src.user_service.requests.get) def test_get_user_failure(self, mock_get): mock_response Mock() mock_response.status_code 404 mock_get.return_value mock_response result get_user_from_api(999) self.assertIsNone(result) mock_get.assert_called_once_with(https://api.example.com/users/999)patch(src.user_service.requests.get)这个装饰器做了两件事在执行test_get_user_success方法时将src.user_service模块中的requests.get替换为一个Mock对象。将这个Mock对象作为参数 (mock_get) 注入到测试方法中供我们配置和断言。你也可以使用patch作为上下文管理器def test_with_context_manager(self): with patch(src.user_service.requests.get) as mock_get: mock_get.return_value Mock(status_code200, jsonlambda: {id: 2}) result get_user_from_api(2) self.assertEqual(result[id], 2)5.3 断言调用行为Mock对象提供了丰富的断言方法来验证其被调用的情况assert_called(): 断言至少被调用过一次。assert_called_once(): 断言被精确调用了一次。assert_called_with(*args, **kwargs): 断言最后一次调用使用了指定的参数。assert_called_once_with(*args, **kwargs): 断言被调用了一次且使用了指定的参数。assert_any_call(*args, **kwargs): 断言曾经以指定参数被调用过不一定是最后一次。assert_has_calls(calls, any_orderFalse): 断言按特定顺序或任意顺序进行了一系列调用。assert_not_called(): 断言从未被调用过。这些断言是验证模块间交互Interaction Testing的关键。5.4 模拟类属性、方法或自身有时你需要模拟一个类的类方法、静态方法甚至模拟这个类本身。from unittest.mock import patch, MagicMock class ExternalService: classmethod def get_config(cls): return {url: real.url} def process(self, data): # 一些复杂的处理 return data * 2 # 测试代码中 class TestMyModule(unittest.TestCase): patch.object(ExternalService, get_config) # 模拟类方法 def test_mock_classmethod(self, mock_get_config): mock_get_config.return_value {url: mock.url} config ExternalService.get_config() self.assertEqual(config[url], mock.url) patch(__main__.ExternalService) # 模拟整个类 def test_mock_class(self, MockService): # 模拟实例 mock_instance MagicMock() mock_instance.process.return_value mocked result MockService.return_value mock_instance # 当代码中创建 ExternalService() 时实际得到的是 mock_instance service ExternalService() result service.process(data) self.assertEqual(result, mocked result) mock_instance.process.assert_called_once_with(data)踩坑实录打补丁的路径patch的第一个参数是关键必须是从被测试代码导入的角度看的路径。一个常见错误是补丁了错误的命名空间。例如在test_module.py中如果你from mymodule import some_func然后在some_func内部调用了external.lib.call()那么你应该补丁mymodule.external.lib.call而不是external.lib.call因为some_func是在mymodule的命名空间下执行的。理解Python的导入系统对正确使用Mock至关重要。6. 进阶技巧与实战中的坑掌握了基础之后一些进阶技巧和实战中遇到的“坑”能让你写出更健壮、更高效的测试。6.1 测试私有方法吗这是一个经典的争论。严格来说单元测试应该只测试公共接口Public API因为私有方法以单下划线_开头是实现细节可能会频繁变动。测试私有方法会让测试变得脆弱且与实现耦合过紧。建议优先通过公共方法来测试私有方法的行为。如果私有方法逻辑极其复杂确实需要独立测试可以考虑以下方法不测试相信通过公共接口的测试已足够覆盖。重构将复杂的私有方法提取到一个独立的公共类或函数中然后测试这个新单元。“不得已”的方法在Python中没有真正的私有。你可以直接调用instance._private_method()。但这只是最后的手段并且要清楚你在测试实现细节。6.2 测试随机性或时间相关代码测试包含random或datetime.now()的代码很棘手因为结果不可预测。解决方法同样是使用Mock。import random import datetime from unittest.mock import patch def generate_id(): return fID-{random.randint(1000, 9999)}-{datetime.datetime.now().strftime(%Y%m%d)} class TestRandomAndTime(unittest.TestCase): patch(random.randint) patch(datetime.datetime) def test_generate_id(self, mock_datetime, mock_randint): # 固定随机数 mock_randint.return_value 1234 # 固定时间 fixed_time datetime.datetime(2023, 10, 27) mock_datetime.now.return_value fixed_time result generate_id() expected ID-1234-20231027 self.assertEqual(result, expected) mock_randint.assert_called_once_with(1000, 9999) mock_datetime.now.assert_called_once()通过补丁random.randint和datetime.datetime.now我们将不确定的因素变成了确定的值。6.3 测试文件I/O测试文件读写时我们同样不希望真的去创建和删除物理文件。可以使用unittest.mock来模拟open函数和文件对象。from unittest.mock import mock_open, patch def read_first_line(filepath): with open(filepath, r) as f: return f.readline().strip() class TestFileIO(unittest.TestCase): patch(builtins.open, new_callablemock_open, read_datafirst line\nsecond line) def test_read_first_line(self, mock_file): result read_first_line(/fake/path/file.txt) self.assertEqual(result, first line) mock_file.assert_called_once_with(/fake/path/file.txt, r)mock_open是一个特殊的Mock类专门用于模拟open函数。read_data参数指定了模拟文件对象read方法返回的内容。6.4 测试异步代码asyncio对于使用asyncio的异步代码unittest也提供了支持主要通过IsolatedAsyncioTestCasePython 3.8。import asyncio import unittest class TestAsyncFunctions(unittest.IsolatedAsyncioTestCase): async def test_async_add(self): async def add(a, b): await asyncio.sleep(0.01) # 模拟一个异步操作 return a b result await add(1, 2) self.assertEqual(result, 3) async def test_async_exception(self): async def raise_error(): raise ValueError(async error) with self.assertRaises(ValueError): await raise_error()继承IsolatedAsyncioTestCase后你的测试方法可以是async def的并且框架会为你管理事件循环。6.5 性能考量不要让测试变慢测试套件应该快速反馈。如果测试太慢开发者就不愿意频繁运行它。避免I/O使用Mock模拟所有网络、数据库、文件操作。使用内存数据库对于必须测试数据库的集成测试使用SQLite内存数据库 (:memory:) 比连接远程数据库快几个数量级。合理使用夹具将耗时的初始化如启动一个测试容器放在setUpClass中而不是每个测试方法的setUp里。并行测试unittest本身不支持并行但你可以使用pytest-xdist插件如果你用pytest或者通过操作系统进程来并行运行测试套件。6.6 测试的命名与可读性好的测试名就是文档。测试方法名应该清晰地描述测试的场景和预期结果。一个流行的命名约定是test_[被测试方法]_[场景]_[预期结果]。例如test_calculate_discount_valid_coupon_returns_20_percent_offtest_login_with_invalid_credentials_raises_authentication_errortest_save_user_with_missing_email_returns_validation_error虽然名字可能很长但在测试失败时你一眼就能看出是哪个功能在什么条件下出了问题。7. 从unittest到pytest为什么很多人迁移虽然unittest功能强大且是标准库但社区中pytest的受欢迎程度已经远超它。了解它们的差异有助于你做技术选型。特性unittest (内置)pytest (第三方)编写风格基于类强制继承TestCase基于函数也支持类。更简洁。断言使用self.assertEqual()等方法使用普通的assert语句失败信息更友好。夹具 (Fixtures)setUp,tearDown,setUpClass,tearDownClass更灵活强大的pytest.fixture装饰器支持依赖注入、作用域函数、类、模块、会话。参数化测试通过parameterized.expand需第三方库或子测试实现原生支持pytest.mark.parametrize非常直观。测试发现规则严格test*.py,test_*方法规则更灵活也能发现unittest测试。插件生态较少极其丰富如并行测试、覆盖率、HTML报告、Mock集成等。输出报告基础文本报告更美观、信息更丰富的报告支持颜色高亮。学习曲线较平缓尤其对有xUnit背景的开发者稍陡但功能强大后效率提升明显。一个简单的对比示例unittest 风格:import unittest class TestMath(unittest.TestCase): def test_addition(self): self.assertEqual(1 1, 2) def test_subtraction(self): self.assertEqual(5 - 3, 2)pytest 风格:# 可以直接是函数 def test_addition(): assert 1 1 2 def test_subtraction(): assert 5 - 3 2 # 也可以用类但不是必须继承特定类 class TestMath: def test_addition(self): assert 1 1 2参数化测试对比unittest (使用子测试):import unittest class TestParam(unittest.TestCase): def test_multiple_cases(self): test_data [(1, 1, 2), (2, 3, 5), (0, 0, 0)] for a, b, expected in test_data: with self.subTest(aa, bb, expectedexpected): self.assertEqual(a b, expected)pytest:import pytest pytest.mark.parametrize(a, b, expected, [(1, 1, 2), (2, 3, 5), (0, 0, 0)]) def test_addition_parametrized(a, b, expected): assert a b expected迁移建议如果你刚开始一个新项目且不介意引入第三方依赖强烈建议直接使用pytest。它的语法更简洁功能更强大社区支持更好。如果你维护一个已有的大型unittest测试代码库没有必要全部重写。pytest可以无缝运行unittest测试用例。你可以逐步在新的测试文件中使用pytest风格。学习unittest仍然有价值因为它教你单元测试的基本概念夹具、断言、模拟这些概念在pytest中同样适用只是表现形式不同。我个人在项目中几乎全部转向了pytest其夹具系统和参数化测试极大地减少了重复代码assert语句的简洁性和丰富的失败信息也让调试变得更容易。但无论如何理解unittest所奠定的基础是写出高质量测试的关键第一步。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进