写在前面
这本书是我们老板推荐过的,我在《价值心法》的推荐书单里也看到了它。用了一段时间 Cursor 软件后,我突然思考,对于测试开发工程师来说,什么才更有价值呢?如何让 AI 工具更好地辅助自己写代码,或许优质的单元测试是一个切入点。 就我个人而言,这本书确实很有帮助。第一次读的时候,很多细节我都不太懂,但将书中内容应用到工作中后,我受益匪浅。比如面对一些让人抓狂的代码设计时,书里的方法能让我逐步深入理解代码的逻辑与设计。 作为一名测试开发工程师,我想把学习这本书的经验分享给大家,希望能给大家带来帮助。因为现在工作中大多使用 Python 代码,所以我把书中JAVA案例都用 Python 代码进行了改写 。
一、TDD 关键模式解析
在测试驱动开发的实践中,多种模式为开发者提供了不同维度的指导,助力高效、高质量的软件开发。
单步测试(One - Step Test)
单步测试旨在从测试列表中选取具有指导意义且可实现的测试,每个测试都应是向最终目标迈进的一步。例如,在一个简易计算器程序的开发中,测试列表可能包含加(Plus)、减(Minus)、乘(Times)、除(Divide)等测试项。
import unittestclass Calculator:def add(self, a, b):return a + bdef subtract(self, a, b):return a - bdef multiply(self, a, b):return a * bdef divide(self, a, b):if b == 0:raise ZeroDivisionErrorreturn a / bclass TestCalculator(unittest.TestCase):def test_add(self):calculator = Calculator()result = calculator.add(3, 5)self.assertEqual(result, 8)def test_subtract(self):calculator = Calculator()result = calculator.subtract(10, 4)self.assertEqual(result, 6)def test_multiply(self):calculator = Calculator()result = calculator.multiply(4, 6)self.assertEqual(result, 24)def test_divide(self):calculator = Calculator()result = calculator.divide(15, 3)self.assertEqual(result, 5)with self.assertRaises(ZeroDivisionError):calculator.divide(10, 0)if __name__ == '__main__':unittest.main()
在这个示例中,每个测试方法都是单步测试的体现,分别验证计算器的一项基本运算功能。开发者可根据自身经验和当前进度,从测试列表中选择合适的测试进行实现,若列表中没有合适的测试,也可添加新的测试来逼近目标。
先导测试(Starter Test)
先导测试强调从一组不做任何事情的操作开始,对于新操作,先明确其所属部分,再编写测试代码。以开发一个图形绘制程序为例,假设要绘制多边形,先导测试可以从输入与输出相同的简单情况开始。
import unittestclass PolygonReducer:def __init__(self, polygon):self.polygon = polygondef result(self):return self.polygonclass TestPolygonReducer(unittest.TestCase):def test_starter(self):polygon = [ (1,1), (2,2), (3,3) ]reducer = PolygonReducer(polygon)result = reducer.result()self.assertEqual(result, polygon)if __name__ == '__main__':unittest.main()
此测试创建了一个简单的多边形对象,并验证了 PolygonReducer
类在初始状态下的输出与输入一致。先导测试适用于确定具有指导意义的起始测试,帮助开发者快速进入开发状态,后续再逐步完成其他测试。
说明性测试(Explanation Test)
在团队开发中,说明性测试可扩展自动化测试的用途。例如,在一个团队协作的项目中,开发一个用户权限管理系统。
class User:def __init__(self, role):self.role = roledef has_permission(self, permission):if self.role == "admin":return Trueelif self.role == "user" and permission == "read":return Truereturn Falseclass TestUserPermissions(unittest.TestCase):def test_permission_explanation(self):admin_user = User("admin")self.assertEqual(admin_user.has_permission("write"), True)regular_user = User("user")self.assertEqual(regular_user.has_permission("write"), False)self.assertEqual(regular_user.has_permission("read"), True)# 为测试添加说明"""对于管理员用户(role = "admin"),应具有所有权限,如 write 权限测试为 True;对于普通用户(role = "user"),仅具有 read 权限,write 权限测试为 False,read 权限测试为 True。"""if __name__ == '__main__':unittest.main()
在上述代码中,不仅对用户权限判断功能进行了测试,还在测试方法中添加了详细的说明,方便团队成员理解测试的意图和预期结果,促进团队对测试和代码的理解,提高测试的质量和可维护性。
学习测试(Learning Test)
当使用外来软件的新功能时,学习测试可用于验证 API 是否按预期工作。以使用 Python 的 sqlite3
模块进行数据库操作学习为例:
import unittest
import sqlite3class TestSQLiteLearning(unittest.TestCase):def setUp(self):self.conn = sqlite3.connect(':memory:')self.cursor = self.conn.cursor()def tearDown(self):self.conn.close()def test_create_table(self):self.cursor.execute('CREATE TABLE users (id INT, name TEXT)')self.conn.commit()self.cursor.execute('SELECT name FROM sqlite_master WHERE type="table" AND name="users"')result = self.cursor.fetchone()self.assertEqual(result[0], "users")if __name__ == '__main__':unittest.main()
在这个测试中,通过编写测试代码来学习 sqlite3
模块创建表的功能。先在 setUp
方法中建立数据库连接,在 test_create_table
方法中执行创建表操作并验证表是否创建成功,最后在 tearDown
方法中关闭数据库连接。通过学习测试,开发者能更好地理解和掌握新软件功能的使用方法。
另外的测试(Another Test)
在技术讨论中,当出现离题想法时,可将其添加到列表中,保持讨论主题的集中。在实际开发中,这意味着将暂时不相关的功能或需求记录下来,专注于当前的开发任务。例如,在开发一个电商网站的过程中,讨论支付功能时,有人提出添加积分兑换功能的想法,可将其记录在列表中,先完成支付功能的开发和测试。
# 假设当前专注于支付功能测试
class PaymentTest(unittest.TestCase):def test_payment(self):# 模拟支付操作result = self.make_payment(100)self.assertEqual(isinstance(result, bool), True)def make_payment(self, amount):# 简单模拟支付返回 True 或 Falsereturn True if amount > 0 else Falseif __name__ == '__main__':unittest.main()
在上述代码中,专注于支付功能的测试,将其他想法暂放一边,保证开发进度和代码质量。
回归测试(Regression Test)
当发现软件缺陷时,编写简单且相应的回归测试,确保缺陷修复且不会引入新问题。例如,在一个字符串处理函数中,原本的函数用于将字符串转换为大写,但发现对于空字符串会出现错误。
import unittestdef to_uppercase(s):if not isinstance(s, str):raise TypeErrorreturn s.upper() if s else ""class TestToUppercase(unittest.TestCase):def test_regression(self):self.assertEqual(to_uppercase("hello"), "HELLO")self.assertEqual(to_uppercase(""), "")with self.assertRaises(TypeError):to_uppercase(123)if __name__ == '__main__':unittest.main()
在修复空字符串处理的问题后,编写回归测试,不仅验证了正常字符串的转换功能,还确保了空字符串和非字符串输入的处理都符合预期,避免了修复缺陷过程中可能引入的新问题。
休息(Break)
当开发者感到疲倦或不知所措时,休息是调整状态的有效方式。例如,在长时间开发一个复杂的人工智能模型时,连续工作数小时后思维变得迟钝,此时离开电脑,喝杯水、散散步,可能会突然想到新的思路来解决之前遇到的问题。从代码层面看,合理安排休息时间有助于保持良好的编码状态,提高代码质量。
重新开始(Do Over)
当感到困惑且经过休息仍无头绪时,可考虑扔掉已有代码重新开始。例如,在开发一个游戏引擎的过程中,由于代码结构逐渐变得混乱,导致新功能开发困难重重,此时可选择重新设计和编写代码。在结对编程中,更换搭档也是促进重新开始的有效方法,新的视角和思路可能会带来更好的解决方案。
工作环境优化
为施行 TDD,舒适的工作环境很重要。比如,选择一把舒适的椅子,保证在长时间编程时的身体舒适度,有助于提高工作效率和专注度。在硬件分配上,将性能最佳的机器用于共享开发,而把低性能的机器用于个人电子邮件服务和上网冲浪等非关键任务,以确保开发工作的顺畅进行。
二、TDD 模式综合应用与思考
这些 TDD 模式相互关联且各有侧重,单步测试和先导测试为开发提供了具体的测试选择和起始思路,帮助开发者逐步推进功能实现;说明性测试和学习测试分别从团队协作和新技术学习的角度,提升了测试的价值和开发者对新功能的掌握程度;另外的测试有助于保持开发的专注性;回归测试保障了代码的稳定性;而休息和重新开始则关注开发者的状态调整,合理的工作环境优化也为 TDD 的顺利实施提供了支持。
在实际项目中,开发者应根据项目的特点、团队的协作方式以及自身的状态,灵活运用这些模式。例如,在开发初期可更多地依赖先导测试和单步测试来搭建功能框架;在团队协作中充分发挥说明性测试的作用;面对新技术或外来软件时,利用学习测试快速上手;同时,时刻关注代码的稳定性,通过回归测试及时发现和解决问题,并合理安排休息和调整开发策略,以实现高效、高质量的测试驱动开发。