Python单元测试unittest从TestCase到Mock的实战指南文章目录Python单元测试unittest从TestCase到Mock的实战指南前言一、第一个测试用例二、核心断言方法断言失败的清晰消息三、setUp与tearDown测试的生命周期四、测试套件与批量运行五、测试覆盖率六、Mock隔离外部依赖6.1 基础Mock对象6.2 实战Mock外部API调用6.3 Patch的三种使用方式6.4 Mock数据库操作七、测试的最佳实践总结✅ 亮点总结适用场景扩展方向前言软件质量的基石不是测试人员的细心而是开发者的自测意识。Python标准库内置的unittest模块脱胎于Java的JUnit提供了完整的单元测试框架测试用例、测试套件、断言方法、前置后置处理以及测试夹具。虽然第三方库pytest更简洁但unittest作为标准库成员零依赖、即开即用仍是最广泛的测试框架。为什么单元测试如此重要因为每一个未经测试的代码行都是一颗定时炸弹——你可能在重构时无意中引入Bug也可能在添加新功能时破坏已有逻辑。单元测试就是你为代码购买的一份保险虽然需要前期投入但每次回归都能给你信心。本文将带你从零掌握unittest的核心用法并深入到Mock对象的实战应用。面试常见考点assertRaises用法、setUp与tearDown的调用时机和顺序、Mock与patch的区别、测试覆盖率的含义。面试官通常不会问你会不会写测试而是问你怎么保证被测代码与外部依赖隔离答案的关键词就是Mock隔离。一、第一个测试用例编写单元测试的第一步永远是确定待测试的代码。被测对象应该是一个相对独立的模块或函数这样测试才能聚焦于单一逻辑。如果被测函数内部还调用了5个其他模块那你写的已经不是单元测试而是集成测试了。我们从一个最简单的计算器类入手# calculator.pyclassCalculator:defadd(self,a,b):returnabdefsubtract(self,a,b):returna-bdefmultiply(self,a,b):returna*bdefdivide(self,a,b):ifb0:raiseValueError(除数不能为零)returna/b对于这个简单的计算器类我们需要验证什么正常运算正数加减、异常输入除零、边界条件乘零。每个测试方法应聚焦一个明确的验证目标这是编写高质量测试的第一原则。如果test_add_positive_numbers方法同时测试了加法和除法失败时你将无法立即定位问题。创建对应的测试文件# test_calculator.pyimportunittestfromcalculatorimportCalculatorclassTestCalculator(unittest.TestCase):defsetUp(self):每个测试方法执行前调用self.calcCalculator()print(f\nsetUp: 创建Calculator实例)deftearDown(self):每个测试方法执行后调用print(ftearDown: 清理资源)deftest_add_positive_numbers(self):测试正数相加resultself.calc.add(3,5)self.assertEqual(result,8)self.assertIsInstance(result,int)deftest_add_negative_numbers(self):测试负数相加resultself.calc.add(-3,-5)self.assertEqual(result,-8)deftest_divide_by_zero_raises_error(self):测试除零抛出异常withself.assertRaises(ValueError)ascontext:self.calc.divide(10,0)self.assertIn(除数不能为零,str(context.exception))deftest_multiply_with_zero(self):测试乘零self.assertEqual(self.calc.multiply(5,0),0)self.assertEqual(self.calc.multiply(0,100),0)if__name____main__:unittest.main(verbosity2)运行测试python test_calculator.py输出包含每个测试的详细结果verbosity2会显示方法名和docstring。这里有个常见的新手陷阱测试方法的命名必须以test_开头否则unittest不会将其识别为测试用例。另外unittest.main()会自动搜索当前模块中以Test开头的类因此在命名测试类时也要遵循这一约定。二、核心断言方法断言是测试的灵魂——没有断言的测试毫无意义。unittest提供了丰富的断言方法覆盖几乎所有验证场景。真正理解每个断言方法的语义比记住它们更重要例如assertEqual(a, b)不仅检查值相等还会在失败时同时显示a和b的实际值方便快速定位差异assertTrue(expr)则只告诉你预期True实际False信息量远不如前者。因此能用assertEqual就不要用assertTrue(a b)classTestAssertions(unittest.TestCase):deftest_all_assertions(self):# 相等性断言self.assertEqual(a,b)# a bself.assertNotEqual(a,b)# a ! b# 布尔断言self.assertTrue(expr)# bool(expr) is Trueself.assertFalse(expr)# bool(expr) is False# 身份断言self.assertIs(a,b)# a is bself.assertIsNot(a,b)# a is not b# 空值断言self.assertIsNone(x)# x is Noneself.assertIsNotNone(x)# x is not None# 成员断言self.assertIn(item,container)# item in containerself.assertNotIn(item,container)# 实例断言self.assertIsInstance(obj,cls)self.assertNotIsInstance(obj,cls)# 近似相等浮点数self.assertAlmostEqual(0.10.2,0.3,places5)self.assertAlmostEqual(100,101,delta2)# 正则匹配self.assertRegex(text,pattern)self.assertNotRegex(text,pattern)# 容器比较self.assertListEqual(list1,list2)self.assertDictEqual(dict1,dict2)self.assertTupleEqual(tuple1,tuple2)self.assertSetEqual(set1,set2)# 异常断言withself.assertRaises(ExpectedException):dangerous_operation()withself.assertWarns(ExpectedWarning):deprecated_function()# 日志断言withself.assertLogs(logger,level)aslog_context:do_something()self.assertIn(expected message,log_context.output[0])断言失败的清晰消息有些开发者觉得断言失败时的默认信息已经够用了但实际上良好的断言消息是快速定位问题的关键。当测试在CI/CD流水线中失败时你看到的可能是一串冰冷的错误输出——此时一条清晰的msg参数就能帮你省下数十分钟的排查时间deftest_user_creation(self):usercreate_user(张三,zhangsanexample.com)self.assertEqual(user.name,张三,msgf期望用户名为张三实际为{user.name})通过msg参数提供自定义错误信息能让失败的测试更容易定位。这个技巧在测试复杂数据结构时尤为实用——比如验证一个用户对象有10个字段你可以在msg中打印出完整的预期值和实际值对比而不仅仅是不相等。三、setUp与tearDown测试的生命周期新手最容易犯的错误之一是将测试依赖关系写在方法之间。每个测试方法必须是完全独立的——修改了数据结构下一个测试不应该感知到。而setUp和tearDown就是实现测试隔离的核心机制。setUp在每个测试前重新初始化状态tearDown负责清理副作用如临时文件、数据库记录。没有这对方法的配合你的测试将变得脆弱且难以维护。unittest提供了三个粒度的准备与清理机制importunittestclassTestLifecycle(unittest.TestCase):classmethoddefsetUpClass(cls):整个测试类开始前执行一次print(setUpClass: 连接数据库)cls.db_connectionfake_db_connectionclassmethoddeftearDownClass(cls):整个测试类结束后执行一次print(tearDownClass: 断开数据库连接)cls.db_connectionNonedefsetUp(self):每个测试方法前执行print( setUp: 创建测试数据)self.test_data[1,2,3]deftearDown(self):每个测试方法后执行print( tearDown: 清理测试数据)self.test_dataNonedeftest_case_one(self):print( 执行 test_case_one)self.assertEqual(len(self.test_data),3)deftest_case_two(self):print( 执行 test_case_two)self.test_data.append(4)self.assertEqual(len(self.test_data),4)执行顺序为setUpClass → setUp → test_case_one → tearDown → setUp → test_case_two → tearDown → tearDownClass关键要点setUpClass/tearDownClass适合一次性的重量级资源数据库连接、测试服务器启动。注意这两个方法需要用classmethod装饰这是常见笔误点。setUp/tearDown确保每个测试方法的独立性互不影响。特别注意如果在tearDown中释放资源时抛出异常标准做法是不要吞掉异常——因为清理失败意味着后续测试可能在不干净的环境中执行这比直接失败更危险。推荐使用addCleanup方法代替tearDown因为它能保证即使setUp抛异常也会执行清理。四、测试套件与批量运行当测试文件只有一个时手动运行很轻松。但当项目有50个测试文件时逐个运行不现实。测试加载器TestLoader的存在就是为了解决这个问题——它自动扫描目录、匹配文件名模式、加载所有测试类。在实际项目中建议将测试运行命令集成到Makefile或CI配置中确保每次提交都自动执行。当测试文件很多时可以用测试套件统一运行# run_tests.pyimportunittest# 方法一自动发现if__name____main__:suiteunittest.TestLoader().discover(start_dirtests,# 测试文件目录patterntest_*.py,# 文件匹配模式top_level_dir.# 顶层目录)runnerunittest.TextTestRunner(verbosity2)runner.run(suite)# 方法二手动组装defcreate_suite():fromtest_calculatorimportTestCalculatorfromtest_userimportTestUser suiteunittest.TestSuite()suite.addTest(unittest.makeSuite(TestCalculator))suite.addTest(unittest.makeSuite(TestUser))returnsuiteif__name____main__:runnerunittest.TextTestRunner(verbosity2)runner.run(create_suite())测试发现Test Discovery是最常用的方式——指定目录和文件名模式自动找到所有测试类。需要注意的是start_dir必须是包包含__init__.py否则discover可能找不到测试模块。另外在大型项目中discover的启动时间可能较长因为它需要遍历整个目录树来导入所有匹配的模块。五、测试覆盖率覆盖率是衡量测试质量的一个量化指标但它绝不是最终目标。100%覆盖率不等于代码没有Bug——你可能测试了所有分支但漏掉了边界条件。覆盖率的真正价值在于告诉你哪些代码从未被执行过从而帮你发现测试盲区。pipinstallcoverage# 运行测试并收集覆盖率coverage run-munittest discover tests# 查看报告coverage report-m# 生成HTML报告coverage html覆盖率报告会告诉你哪些代码行被测试覆盖了哪些还没被触及。六、Mock隔离外部依赖真正的单元测试应该隔离外部依赖——这是区分单元测试和集成测试的分水岭。如果你的测试真的去连数据库、调API、发邮件那它不仅是缓慢的更是脆弱的依赖外部状态。unittest.mock提供的Mock和patch两大核心工具让你可以用替身替换这些外部依赖让测试聚焦于被测试代码内部逻辑。这里的核心理念是我们假设外部依赖是正确的它们自有独立的测试我们只验证自己代码与它们的交互是否正确。6.1 基础Mock对象Mock对象就像一个百变怪——你可以动态给它附加任意属性、方法并预设这些方法的返回值。它还能记录自己被调用的历史调用了多少次、传入什么参数这正是验证代码行为的核心。以下示例展示了Mock的最基本能力fromunittest.mockimportMock# Mock对象可以模拟任意属性和方法fake_objMock()fake_obj.nametestfake_obj.get_data.return_value{status:ok}print(fake_obj.name)# testprint(fake_obj.get_data())# {status: ok}# 验证方法被调用fake_obj.get_data()fake_obj.get_data.assert_called_once()fake_obj.get_data.assert_called_with()6.2 实战Mock外部API调用# weather.pyimportrequestsclassWeatherService:BASE_URLhttps://api.weather.com/v1defget_temperature(self,city):responserequests.get(f{self.BASE_URL}/current,params{city:city},timeout5)ifresponse.status_code200:returnresponse.json().get(temperature)raiseException(fAPI请求失败:{response.status_code})# test_weather.pyimportunittestfromunittest.mockimportpatch,MockfromweatherimportWeatherServiceclassTestWeatherService(unittest.TestCase):patch(weather.requests.get)deftest_get_temperature_success(self,mock_get):测试成功获取温度# 配置Mock返回值mock_responseMock()mock_response.status_code200mock_response.json.return_value{temperature:25.5,humidity:60}mock_get.return_valuemock_response serviceWeatherService()tempservice.get_temperature(Beijing)self.assertEqual(temp,25.5)# 验证API被正确调用mock_get.assert_called_once_with(https://api.weather.com/v1/current,params{city:Beijing},timeout5)patch(weather.requests.get)deftest_get_temperature_api_failure(self,mock_get):测试API请求失败mock_responseMock()mock_response.status_code500mock_get.return_valuemock_response serviceWeatherService()withself.assertRaises(Exception)ascontext:service.get_temperature(Beijing)self.assertIn(API请求失败,str(context.exception))patch(weather.requests.get)deftest_get_temperature_network_error(self,mock_get):测试网络异常mock_get.side_effectConnectionError(网络连接超时)serviceWeatherService()withself.assertRaises(ConnectionError):service.get_temperature(Beijing)6.3 Patch的三种使用方式选择哪种patch方式取决于你的场景装饰器最简洁适合整个测试方法都需要mock的情况上下文管理器适合只在部分代码段中mock手动start/stop则适合setUp/tearDown模式当多个测试方法需要相同的mock时可以集中管理。一个常见陷阱是patch的路径写错——必须patch在被测试模块的导入路径上而非目标模块的定义位置。# 方式一装饰器patch(module.external_function)deftest_foo(self,mock_func):mock_func.return_value42...# 方式二上下文管理器deftest_bar(self):withpatch(module.external_function)asmock_func:mock_func.return_value42...# 方式三手动控制deftest_baz(self):patcherpatch(module.external_function)mock_funcpatcher.start()mock_func.return_value42...patcher.stop()6.4 Mock数据库操作Mock数据库是Mock应用的最典型场景。数据库查询通常是测试中最耗时的部分而且需要预先插入测试数据、测试后清理这让测试变得复杂且缓慢。通过Mock取代数据库调用测试可以在毫秒级完成并且完全可重复。注意MagicMock是Mock的子类支持Python魔术方法如__enter__、__iter__当需要mock上下文管理器或迭代器时应使用MagicMock而非普通Mock。fromunittest.mockimportpatch,MagicMockclassTestUserRepository(unittest.TestCase):patch(mymodule.DatabaseConnection)deftest_find_user_by_id(self,MockDB):Mock数据库查询# 配置Mock链mock_sessionMagicMock()MockDB.return_value.__enter__.return_valuemock_session user_repoUserRepository()mock_session.execute.return_value.fetchone.return_value{id:1,name:张三}useruser_repo.find_by_id(1)self.assertEqual(user[name],张三)mock_session.execute.assert_called_once()七、测试的最佳实践一个测试只测一件事测试方法名应清晰描述测试场景如test_divide_by_zero_raises_value_error测试覆盖边界条件不仅要测正常路径还要测空值、零值、极大值、异常输入AAA模式Arrange准备→Act执行→Assert断言FIRST原则Fast快速、Independent独立、Repeatable可重复、Self-validating自验证、Timely及时编写不要测试实现细节测试行为而非内部实现这样重构时测试不会失效总结本文系统地介绍了Python unittest框架的完整用法从TestCase创建和断言方法到setUp/tearDown管理测试生命周期再到Mock对象隔离外部依赖实现真正的单元测试。Mock是单元测试的灵魂——没有它你的测试将不再是单元测试而是集成测试。建议读者立即在项目中实践先为所有核心函数编写测试再为边界条件补充测试用例最后用覆盖率工具检查遗漏。下一篇我们将讨论Python代码规范与风格指南学习如何写出让同行点赞的整洁代码。✅ 亮点总结TestCase提供assertEqual、assertRaises、assertTrue等二十余种断言方法覆盖各类验证需求setUp()/tearDown()在每个测试前后自动执行确保用例之间独立无干扰Mockunittest.mock是单元测试的灵魂——它隔离外部依赖让测试真正聚焦于被测试的逻辑本身AAA模式Arrange-Act-Assert提供了清晰的测试代码结构任何开发者一看就懂覆盖率工具coverage.py量化测试质量明确告诉你还剩多少代码未经测试适用场景CI/CD流水线在每次代码提交时自动运行测试套件阻止有问题的代码合并到主分支重构安全网在对遗留代码进行重构前先补齐测试用例确保改后行为不变API接口测试用Mock模拟HTTP请求和数据库调用在不启动真实服务的情况下验证接口逻辑扩展方向pytest框架进阶学习pytest的fixture、参数化和插件体系体验比unittest更简洁的测试写法测试驱动开发TDD实践先写测试、再写代码、最后重构的开发方法论推荐阅读下一篇《Python代码规范与风格指南》学习PEP 8规范写出整洁优雅的Python代码测试不是负担而是一种投资。它让重构不再恐惧让发布充满信心。
88_Python单元测试unittest
发布时间:2026/7/10 22:56:32
Python单元测试unittest从TestCase到Mock的实战指南文章目录Python单元测试unittest从TestCase到Mock的实战指南前言一、第一个测试用例二、核心断言方法断言失败的清晰消息三、setUp与tearDown测试的生命周期四、测试套件与批量运行五、测试覆盖率六、Mock隔离外部依赖6.1 基础Mock对象6.2 实战Mock外部API调用6.3 Patch的三种使用方式6.4 Mock数据库操作七、测试的最佳实践总结✅ 亮点总结适用场景扩展方向前言软件质量的基石不是测试人员的细心而是开发者的自测意识。Python标准库内置的unittest模块脱胎于Java的JUnit提供了完整的单元测试框架测试用例、测试套件、断言方法、前置后置处理以及测试夹具。虽然第三方库pytest更简洁但unittest作为标准库成员零依赖、即开即用仍是最广泛的测试框架。为什么单元测试如此重要因为每一个未经测试的代码行都是一颗定时炸弹——你可能在重构时无意中引入Bug也可能在添加新功能时破坏已有逻辑。单元测试就是你为代码购买的一份保险虽然需要前期投入但每次回归都能给你信心。本文将带你从零掌握unittest的核心用法并深入到Mock对象的实战应用。面试常见考点assertRaises用法、setUp与tearDown的调用时机和顺序、Mock与patch的区别、测试覆盖率的含义。面试官通常不会问你会不会写测试而是问你怎么保证被测代码与外部依赖隔离答案的关键词就是Mock隔离。一、第一个测试用例编写单元测试的第一步永远是确定待测试的代码。被测对象应该是一个相对独立的模块或函数这样测试才能聚焦于单一逻辑。如果被测函数内部还调用了5个其他模块那你写的已经不是单元测试而是集成测试了。我们从一个最简单的计算器类入手# calculator.pyclassCalculator:defadd(self,a,b):returnabdefsubtract(self,a,b):returna-bdefmultiply(self,a,b):returna*bdefdivide(self,a,b):ifb0:raiseValueError(除数不能为零)returna/b对于这个简单的计算器类我们需要验证什么正常运算正数加减、异常输入除零、边界条件乘零。每个测试方法应聚焦一个明确的验证目标这是编写高质量测试的第一原则。如果test_add_positive_numbers方法同时测试了加法和除法失败时你将无法立即定位问题。创建对应的测试文件# test_calculator.pyimportunittestfromcalculatorimportCalculatorclassTestCalculator(unittest.TestCase):defsetUp(self):每个测试方法执行前调用self.calcCalculator()print(f\nsetUp: 创建Calculator实例)deftearDown(self):每个测试方法执行后调用print(ftearDown: 清理资源)deftest_add_positive_numbers(self):测试正数相加resultself.calc.add(3,5)self.assertEqual(result,8)self.assertIsInstance(result,int)deftest_add_negative_numbers(self):测试负数相加resultself.calc.add(-3,-5)self.assertEqual(result,-8)deftest_divide_by_zero_raises_error(self):测试除零抛出异常withself.assertRaises(ValueError)ascontext:self.calc.divide(10,0)self.assertIn(除数不能为零,str(context.exception))deftest_multiply_with_zero(self):测试乘零self.assertEqual(self.calc.multiply(5,0),0)self.assertEqual(self.calc.multiply(0,100),0)if__name____main__:unittest.main(verbosity2)运行测试python test_calculator.py输出包含每个测试的详细结果verbosity2会显示方法名和docstring。这里有个常见的新手陷阱测试方法的命名必须以test_开头否则unittest不会将其识别为测试用例。另外unittest.main()会自动搜索当前模块中以Test开头的类因此在命名测试类时也要遵循这一约定。二、核心断言方法断言是测试的灵魂——没有断言的测试毫无意义。unittest提供了丰富的断言方法覆盖几乎所有验证场景。真正理解每个断言方法的语义比记住它们更重要例如assertEqual(a, b)不仅检查值相等还会在失败时同时显示a和b的实际值方便快速定位差异assertTrue(expr)则只告诉你预期True实际False信息量远不如前者。因此能用assertEqual就不要用assertTrue(a b)classTestAssertions(unittest.TestCase):deftest_all_assertions(self):# 相等性断言self.assertEqual(a,b)# a bself.assertNotEqual(a,b)# a ! b# 布尔断言self.assertTrue(expr)# bool(expr) is Trueself.assertFalse(expr)# bool(expr) is False# 身份断言self.assertIs(a,b)# a is bself.assertIsNot(a,b)# a is not b# 空值断言self.assertIsNone(x)# x is Noneself.assertIsNotNone(x)# x is not None# 成员断言self.assertIn(item,container)# item in containerself.assertNotIn(item,container)# 实例断言self.assertIsInstance(obj,cls)self.assertNotIsInstance(obj,cls)# 近似相等浮点数self.assertAlmostEqual(0.10.2,0.3,places5)self.assertAlmostEqual(100,101,delta2)# 正则匹配self.assertRegex(text,pattern)self.assertNotRegex(text,pattern)# 容器比较self.assertListEqual(list1,list2)self.assertDictEqual(dict1,dict2)self.assertTupleEqual(tuple1,tuple2)self.assertSetEqual(set1,set2)# 异常断言withself.assertRaises(ExpectedException):dangerous_operation()withself.assertWarns(ExpectedWarning):deprecated_function()# 日志断言withself.assertLogs(logger,level)aslog_context:do_something()self.assertIn(expected message,log_context.output[0])断言失败的清晰消息有些开发者觉得断言失败时的默认信息已经够用了但实际上良好的断言消息是快速定位问题的关键。当测试在CI/CD流水线中失败时你看到的可能是一串冰冷的错误输出——此时一条清晰的msg参数就能帮你省下数十分钟的排查时间deftest_user_creation(self):usercreate_user(张三,zhangsanexample.com)self.assertEqual(user.name,张三,msgf期望用户名为张三实际为{user.name})通过msg参数提供自定义错误信息能让失败的测试更容易定位。这个技巧在测试复杂数据结构时尤为实用——比如验证一个用户对象有10个字段你可以在msg中打印出完整的预期值和实际值对比而不仅仅是不相等。三、setUp与tearDown测试的生命周期新手最容易犯的错误之一是将测试依赖关系写在方法之间。每个测试方法必须是完全独立的——修改了数据结构下一个测试不应该感知到。而setUp和tearDown就是实现测试隔离的核心机制。setUp在每个测试前重新初始化状态tearDown负责清理副作用如临时文件、数据库记录。没有这对方法的配合你的测试将变得脆弱且难以维护。unittest提供了三个粒度的准备与清理机制importunittestclassTestLifecycle(unittest.TestCase):classmethoddefsetUpClass(cls):整个测试类开始前执行一次print(setUpClass: 连接数据库)cls.db_connectionfake_db_connectionclassmethoddeftearDownClass(cls):整个测试类结束后执行一次print(tearDownClass: 断开数据库连接)cls.db_connectionNonedefsetUp(self):每个测试方法前执行print( setUp: 创建测试数据)self.test_data[1,2,3]deftearDown(self):每个测试方法后执行print( tearDown: 清理测试数据)self.test_dataNonedeftest_case_one(self):print( 执行 test_case_one)self.assertEqual(len(self.test_data),3)deftest_case_two(self):print( 执行 test_case_two)self.test_data.append(4)self.assertEqual(len(self.test_data),4)执行顺序为setUpClass → setUp → test_case_one → tearDown → setUp → test_case_two → tearDown → tearDownClass关键要点setUpClass/tearDownClass适合一次性的重量级资源数据库连接、测试服务器启动。注意这两个方法需要用classmethod装饰这是常见笔误点。setUp/tearDown确保每个测试方法的独立性互不影响。特别注意如果在tearDown中释放资源时抛出异常标准做法是不要吞掉异常——因为清理失败意味着后续测试可能在不干净的环境中执行这比直接失败更危险。推荐使用addCleanup方法代替tearDown因为它能保证即使setUp抛异常也会执行清理。四、测试套件与批量运行当测试文件只有一个时手动运行很轻松。但当项目有50个测试文件时逐个运行不现实。测试加载器TestLoader的存在就是为了解决这个问题——它自动扫描目录、匹配文件名模式、加载所有测试类。在实际项目中建议将测试运行命令集成到Makefile或CI配置中确保每次提交都自动执行。当测试文件很多时可以用测试套件统一运行# run_tests.pyimportunittest# 方法一自动发现if__name____main__:suiteunittest.TestLoader().discover(start_dirtests,# 测试文件目录patterntest_*.py,# 文件匹配模式top_level_dir.# 顶层目录)runnerunittest.TextTestRunner(verbosity2)runner.run(suite)# 方法二手动组装defcreate_suite():fromtest_calculatorimportTestCalculatorfromtest_userimportTestUser suiteunittest.TestSuite()suite.addTest(unittest.makeSuite(TestCalculator))suite.addTest(unittest.makeSuite(TestUser))returnsuiteif__name____main__:runnerunittest.TextTestRunner(verbosity2)runner.run(create_suite())测试发现Test Discovery是最常用的方式——指定目录和文件名模式自动找到所有测试类。需要注意的是start_dir必须是包包含__init__.py否则discover可能找不到测试模块。另外在大型项目中discover的启动时间可能较长因为它需要遍历整个目录树来导入所有匹配的模块。五、测试覆盖率覆盖率是衡量测试质量的一个量化指标但它绝不是最终目标。100%覆盖率不等于代码没有Bug——你可能测试了所有分支但漏掉了边界条件。覆盖率的真正价值在于告诉你哪些代码从未被执行过从而帮你发现测试盲区。pipinstallcoverage# 运行测试并收集覆盖率coverage run-munittest discover tests# 查看报告coverage report-m# 生成HTML报告coverage html覆盖率报告会告诉你哪些代码行被测试覆盖了哪些还没被触及。六、Mock隔离外部依赖真正的单元测试应该隔离外部依赖——这是区分单元测试和集成测试的分水岭。如果你的测试真的去连数据库、调API、发邮件那它不仅是缓慢的更是脆弱的依赖外部状态。unittest.mock提供的Mock和patch两大核心工具让你可以用替身替换这些外部依赖让测试聚焦于被测试代码内部逻辑。这里的核心理念是我们假设外部依赖是正确的它们自有独立的测试我们只验证自己代码与它们的交互是否正确。6.1 基础Mock对象Mock对象就像一个百变怪——你可以动态给它附加任意属性、方法并预设这些方法的返回值。它还能记录自己被调用的历史调用了多少次、传入什么参数这正是验证代码行为的核心。以下示例展示了Mock的最基本能力fromunittest.mockimportMock# Mock对象可以模拟任意属性和方法fake_objMock()fake_obj.nametestfake_obj.get_data.return_value{status:ok}print(fake_obj.name)# testprint(fake_obj.get_data())# {status: ok}# 验证方法被调用fake_obj.get_data()fake_obj.get_data.assert_called_once()fake_obj.get_data.assert_called_with()6.2 实战Mock外部API调用# weather.pyimportrequestsclassWeatherService:BASE_URLhttps://api.weather.com/v1defget_temperature(self,city):responserequests.get(f{self.BASE_URL}/current,params{city:city},timeout5)ifresponse.status_code200:returnresponse.json().get(temperature)raiseException(fAPI请求失败:{response.status_code})# test_weather.pyimportunittestfromunittest.mockimportpatch,MockfromweatherimportWeatherServiceclassTestWeatherService(unittest.TestCase):patch(weather.requests.get)deftest_get_temperature_success(self,mock_get):测试成功获取温度# 配置Mock返回值mock_responseMock()mock_response.status_code200mock_response.json.return_value{temperature:25.5,humidity:60}mock_get.return_valuemock_response serviceWeatherService()tempservice.get_temperature(Beijing)self.assertEqual(temp,25.5)# 验证API被正确调用mock_get.assert_called_once_with(https://api.weather.com/v1/current,params{city:Beijing},timeout5)patch(weather.requests.get)deftest_get_temperature_api_failure(self,mock_get):测试API请求失败mock_responseMock()mock_response.status_code500mock_get.return_valuemock_response serviceWeatherService()withself.assertRaises(Exception)ascontext:service.get_temperature(Beijing)self.assertIn(API请求失败,str(context.exception))patch(weather.requests.get)deftest_get_temperature_network_error(self,mock_get):测试网络异常mock_get.side_effectConnectionError(网络连接超时)serviceWeatherService()withself.assertRaises(ConnectionError):service.get_temperature(Beijing)6.3 Patch的三种使用方式选择哪种patch方式取决于你的场景装饰器最简洁适合整个测试方法都需要mock的情况上下文管理器适合只在部分代码段中mock手动start/stop则适合setUp/tearDown模式当多个测试方法需要相同的mock时可以集中管理。一个常见陷阱是patch的路径写错——必须patch在被测试模块的导入路径上而非目标模块的定义位置。# 方式一装饰器patch(module.external_function)deftest_foo(self,mock_func):mock_func.return_value42...# 方式二上下文管理器deftest_bar(self):withpatch(module.external_function)asmock_func:mock_func.return_value42...# 方式三手动控制deftest_baz(self):patcherpatch(module.external_function)mock_funcpatcher.start()mock_func.return_value42...patcher.stop()6.4 Mock数据库操作Mock数据库是Mock应用的最典型场景。数据库查询通常是测试中最耗时的部分而且需要预先插入测试数据、测试后清理这让测试变得复杂且缓慢。通过Mock取代数据库调用测试可以在毫秒级完成并且完全可重复。注意MagicMock是Mock的子类支持Python魔术方法如__enter__、__iter__当需要mock上下文管理器或迭代器时应使用MagicMock而非普通Mock。fromunittest.mockimportpatch,MagicMockclassTestUserRepository(unittest.TestCase):patch(mymodule.DatabaseConnection)deftest_find_user_by_id(self,MockDB):Mock数据库查询# 配置Mock链mock_sessionMagicMock()MockDB.return_value.__enter__.return_valuemock_session user_repoUserRepository()mock_session.execute.return_value.fetchone.return_value{id:1,name:张三}useruser_repo.find_by_id(1)self.assertEqual(user[name],张三)mock_session.execute.assert_called_once()七、测试的最佳实践一个测试只测一件事测试方法名应清晰描述测试场景如test_divide_by_zero_raises_value_error测试覆盖边界条件不仅要测正常路径还要测空值、零值、极大值、异常输入AAA模式Arrange准备→Act执行→Assert断言FIRST原则Fast快速、Independent独立、Repeatable可重复、Self-validating自验证、Timely及时编写不要测试实现细节测试行为而非内部实现这样重构时测试不会失效总结本文系统地介绍了Python unittest框架的完整用法从TestCase创建和断言方法到setUp/tearDown管理测试生命周期再到Mock对象隔离外部依赖实现真正的单元测试。Mock是单元测试的灵魂——没有它你的测试将不再是单元测试而是集成测试。建议读者立即在项目中实践先为所有核心函数编写测试再为边界条件补充测试用例最后用覆盖率工具检查遗漏。下一篇我们将讨论Python代码规范与风格指南学习如何写出让同行点赞的整洁代码。✅ 亮点总结TestCase提供assertEqual、assertRaises、assertTrue等二十余种断言方法覆盖各类验证需求setUp()/tearDown()在每个测试前后自动执行确保用例之间独立无干扰Mockunittest.mock是单元测试的灵魂——它隔离外部依赖让测试真正聚焦于被测试的逻辑本身AAA模式Arrange-Act-Assert提供了清晰的测试代码结构任何开发者一看就懂覆盖率工具coverage.py量化测试质量明确告诉你还剩多少代码未经测试适用场景CI/CD流水线在每次代码提交时自动运行测试套件阻止有问题的代码合并到主分支重构安全网在对遗留代码进行重构前先补齐测试用例确保改后行为不变API接口测试用Mock模拟HTTP请求和数据库调用在不启动真实服务的情况下验证接口逻辑扩展方向pytest框架进阶学习pytest的fixture、参数化和插件体系体验比unittest更简洁的测试写法测试驱动开发TDD实践先写测试、再写代码、最后重构的开发方法论推荐阅读下一篇《Python代码规范与风格指南》学习PEP 8规范写出整洁优雅的Python代码测试不是负担而是一种投资。它让重构不再恐惧让发布充满信心。