本文大纲
《我的敏捷软件项目管理实践》
单元测试
单元测试是软件开发中确保代码质量的重要手段,它通过验证每个功能模块的正确性,帮助开发者快速发现并修复问题。敏捷方法中的 XP(极限编程)提出的“测试驱动开发”的思想,强调先写测试后写代码。虽然我们团队是后写测试的,但我依然十分重视单元测试。
我所在的敏捷团队,运行效率很高,但测试人员与开发人员的 比例较低。这并不是说测试不重要,而是敏捷团队保证质量的责任并不仅限于测试人员。整个敏捷团队都在协同提高产品质量,这与传统团队依赖QA小组把控质量形成鲜明对比。例如微软和谷歌的人员配比差异显著:微软开发与测试比例通常为1:1到2:1,而谷歌可达10:1。谷歌倡导开发主导测试,测试团队聚焦提供自动化工具支持。我过往任职公司的开发测试比为1:1到2:1,而当前公司达到5:1甚至10:1,仍能高效运转,这主要得益于严格的单元测试、清晰的设计规范、标准化的数据模型,以及有效的代码审查机制,能在开发阶段消除大量缺陷。同时通过提升自动化测试覆盖率,针对旧有功能尽量使用自动化测试,将测试人员精力集中在新功能验证上。
在敏捷团队里,测试人员岗位名称叫QA翻译过来是“质量保障”,QA人员的首要价值不是找出你低级的bug,开发人员应独立交付高质量的代码。QA人员能够创造更大的价值,他们做探索性测试、创建测试自动化、与产品负责人紧密合作完善需求和验收条件。
干货
- 每个开发工程师都需要树立“交付合格代码”的理想信念,通过单元测试来证明代码合格是一种好方法。公司招聘你不是为了制造Bug,测试人员也不应该为低级错误买单。努力追求零缺陷的代码质量才是目标。
- 虽然编写单元测试需要花费时间,但相比测试人员通过黑盒测试发现Bug,前者所花费的时间更少。问题发现得越早,修复的成本就越低。
- 过去,大家不愿编写单元测试,因为它耗时较多。如今,随着代码生成工具和AI工具的发展,这些工具极大地提高了单元测试的编写效率,显著减少了所需时间。尽管编写单元测试需要时间,但它减少了修复线上Bug的时间,总体上更加高效。
- 测试人员通常只对新功能进行重点测试,而对旧有功能则根据影响分析进行回归测试。由于时间成本原因,难免会有遗漏。针对旧有功能,运行所有测试以验证新代码未破坏其功能时,这种踏实感令人安心。
- 通过“5种边界值”等测试思想编写单元测试,可以发现手动测试难以察觉的隐蔽问题。
- 在代码审查时,审查单元测试也是其中的一个环节,开发人员主动报告单元测试结果。
- “不要在单元测试上偷懒”请跟我把这句话念10遍,当你一口气改完几十个类文件然后执行一个测试命令几秒内跑完所有用例,满屏的绿条给你带来的快感会让你瞬间升入天堂。
单元测试的基本原则
测试独立性
- 单个功能:每个单元测试应只测试一个功能点,避免依赖其他模块。
- 隔离环境:使用Mock对象或桩(stubs)模拟外部依赖,确保测试环境独立可控。
快速反馈
- 快速执行:单元测试应能在几秒钟内完成,提供即时反馈。
- 自动化运行:集成到持续集成(CI)系统中,确保每次代码提交后自动运行测试。
可重复性
- 一致结果:无论何时运行,相同的输入应产生相同的结果。
- 幂等性:多次运行测试不会影响系统的状态。
测试数据的选择
测试的5种值
根据不同的业务场景,选择合适的测试数据,确保覆盖各种情况:
- 正常值:最常见的输入,验证功能在预期条件下正常工作。
- 空值:验证程序能否正确处理空输入。
- 最大值、最小值:测试边界条件,确保程序能处理极端值。
- 错误值:输入无效或异常的数据,验证错误处理机制。
- 临界值:接近边界条件的值,确保程序在临界状态下表现正确。
边界条件用例
- 金额精度溢出:测试涉及金额的操作时,确保不会因为精度问题导致溢出。
- 字符串长度限制:验证字符串字段的最大长度限制是否有效。
- 数组索引越界:确保数组操作不会导致索引越界异常。
单元测试策略
边界条件测试
- 定义边界:明确每个功能的边界条件,确保所有可能的边界情况都被测试。
- 示例:
- 支付模块中,测试金额为0、最小单位(如0.01元)、最大支持金额。
- 用户名长度为1、最大允许长度、超过最大长度等情况。
幂等性验证
- 重复调用:验证同一操作多次调用是否产生相同的结果。
- 示例:
- 支付模块中,重复发起支付请求,确保只有一次成功支付。
- 更新用户信息时,多次调用更新接口,确保用户信息不变。
故障注入测试
- 模拟故障:通过模拟网络分区、数据库连接失败等异常情况,验证系统的容错能力。
- 示例:
- 模拟网络延迟或断开,测试支付回调是否能正确处理。
- 模拟数据库不可用,测试系统是否能优雅降级或重试。
编写高效的单元测试
使用测试框架
- 选择框架:根据编程语言选择合适的测试框架,如JUnit(Java)、pytest(Python)、Mocha(JavaScript)等。
- 组织结构:按照模块和功能点组织测试文件和函数,保持清晰的目录结构。
编写可读性强的测试用例
- 命名规范:测试函数名称应清晰描述测试内容,如
test_user_registration_with_invalid_email。 - 注释说明:为复杂测试用例添加注释,解释测试逻辑和预期结果。
使用Mock和Stub
- Mock对象:模拟外部依赖,如数据库、API调用等,确保测试环境纯净。
- 示例:
- 使用Mockito(Java)或unittest.mock(Python)模拟数据库查询。
- 使用WireMock模拟HTTP请求和响应。
测试覆盖率
- 工具支持:使用工具(如JaCoCo、Coverage.py)监控测试覆盖率,确保关键路径得到充分测试。
- 目标设定:设定合理的覆盖率目标(如80%),但不盲目追求100%。
集成与维护
持续集成
- 自动运行:将单元测试集成到CI/CD管道中,确保每次代码提交后自动运行测试。
- 快速反馈:及时获取测试结果,快速定位和修复问题。
测试维护
- 定期审查:定期审查和优化测试用例,删除过时或冗余的测试。
- 文档记录:为复杂的测试用例编写文档,方便新成员理解和维护。