写在前面
这本书是我们老板推荐过的,我在《价值心法》的推荐书单里也看到了它。用了一段时间 Cursor 软件后,我突然思考,对于测试开发工程师来说,什么才更有价值呢?如何让 AI 工具更好地辅助自己写代码,或许优质的单元测试是一个切入点。 就我个人而言,这本书确实很有帮助。第一次读的时候,很多细节我都不太懂,但将书中内容应用到工作中后,我受益匪浅。比如面对一些让人抓狂的代码设计时,书里的方法能让我逐步深入理解代码的逻辑与设计。 作为一名测试开发工程师,我想把学习这本书的经验分享给大家,希望能给大家带来帮助。因为现在工作中大多使用 Python 代码,所以我把书中JAVA案例都用 Python 代码进行了改写 。
在软件开发领域,测试驱动开发(TDD)凭借其保障代码质量的优势,成为众多开发者青睐的实践方式。xUnit作为TDD中常用的测试框架家族,有着丰富的内涵与应用价值。在上一篇博客中,我们深入了解了xUnit测试框架的基础结构、代码示例以及部分待办事项。今天,我们将基于书中的进一步内容,继续探讨xUnit在测试驱动开发中的应用细节。
测试编写的一般化形式:3A原则
当开发者开始编写测试时,会发现一种具有通用性的形式,Bill Wake将其总结为3A原则。这一原则为测试的编写提供了清晰的逻辑步骤:
- 筹划(Arrange):此步骤旨在创建若干对象,精心为测试准备好所需的初始条件和环境。就好比在进行一场数学运算测试前,我们要提前准备好参与运算的数字对象,为后续的测试操作搭建好舞台。
- 行为(Act):在准备工作就绪后,激活这些对象,使其执行相应的操作。例如,让准备好的数字对象进行加法、减法等运算,促使测试对象产生行为和输出。
- 断言(Assert):最后,通过验证结果来判断测试是否成功。具体来说,就是检查对象操作后的输出是否符合我们的预期。比如验证两个数字相加的结果是否正确,以此来确定测试的有效性。
在这个模式中,筹划步骤对于每个测试通常具有相似性,它为测试奠定基础。而行为和断言步骤则因具体测试内容的不同而各异。例如,数字7和9进行不同运算(加、减、乘),虽然运算和期望结果各不相同,但数字本身的准备过程是类似的,这体现了3A原则的灵活性和普适性。
测试中的性能与隔离矛盾
在不同规模下重复使用上述测试模式时,开发者会面临性能和隔离这两条相互矛盾的约束。这两个方面在测试过程中都至关重要,但却难以同时兼顾:
- 性能:从提高测试效率的角度出发,我们希望测试运行得尽可能快。因此,如果在众多测试中使用类似对象,我们期望这些对象在所有测试中只创建一次,以减少频繁创建对象带来的开销,从而提升测试的整体运行速度。
- 隔离:为了确保测试的准确性和可靠性,我们期望某个测试无论通过还是失败,都与其他测试无关。因为若测试间共享对象且某个测试改变了这些对象,后续测试运行结果可能会受到影响,即出现测试耦合。测试耦合存在明显不良作用,可能导致中断一个测试会引起后续多个测试失败,而且测试顺序也会对结果产生微妙影响,这使得测试结果变得不可靠和难以预测。
解决问题的实践:setUp方法的运用
为解决上述性能与隔离的矛盾问题,以设置标志和简化测试为例,开发者进行了一系列实践:
- 最初,为确保某个测试运行前的准备工作已完成,希望有一个标志来进行标识。例如,在测试类WasRun中,期望在运行测试之前设置一个表示已准备好的标志。通过编写TestCaseTest类的testSetUp方法进行测试,但运行时Python提示未发现相关属性,这是因为还未对该标志进行设置,暴露出测试准备工作的缺失。
- 随后,在WasRun类中定义setUp方法来设置该标志,如
self.wasSetUp = 1
。但调用setUp方法从职责上来说是TestCase类的任务,所以进一步查看TestCase类,发现其setUp方法目前为空实现,而run方法中会调用setUp方法,这为后续完善测试准备工作提供了切入点。 - 进一步地,可以直接使用新添加的属性来简化测试,如在WasRun类的setUp方法中设置
self.wasRun = None
等。同时,也可以简化测试本身,在TestCaseTest类的setUp方法中创建WasRun实例,并在测试方法中使用它,以确保测试间不会存在耦合(假设对象间不会通过特殊方式相互影响)。通过这些逐步的改进,使得测试的逻辑更加清晰,耦合度降低,同时也提高了测试的可维护性。
示例代码解析
以下是结合前面内容的示例代码,通过代码可以更直观地理解相关概念和功能的实现:
class WasRun:def __init__(self, name):self.name = nameself.wasRun = Noneself.wasSetUp = Nonedef setUp(self):self.wasSetUp = 1self.wasRun = Nonedef testMethod(self):self.wasRun = 1class TestCase:def __init__(self, name):self.name = namedef setUp(self):passdef tearDown(self):passdef run(self, result=None):if result is None:result = TestResult()result.testStarted()self.setUp()try:method = getattr(self, self.name)method()except:result.testFailed()self.tearDown()return resultclass TestResult:def __init__(self):self.runCount = 0self.errorCount = 0def testStarted(self):self.runCount = self.runCount + 1def testFailed(self):self.errorCount = self.errorCount + 1def summary(self):return "%d run, %d failed" % (self.runCount, self.errorCount)class TestCaseTest(TestCase):def setUp(self):self.result = TestResult()self.test = WasRun("testMethod")def testSetUp(self):self.test.run()assert self.test.wasSetUp == 1def testRunning(self):self.test.run()assert self.test.wasRun == 1if __name__ == "__main__":suite = TestCaseTest("testSetUp")result = suite.run()print(result.summary())suite = TestCaseTest("testRunning")result = suite.run()print(result.summary())
代码说明
- WasRun类:
__init__
方法初始化测试名称、wasRun
(用于标记测试方法是否执行)和wasSetUp
(用于标记setUp
方法是否执行)属性。setUp
方法设置wasSetUp
为1,并重置wasRun
为None
,模拟测试前的准备工作。testMethod
方法模拟实际的测试逻辑,执行后设置wasRun
为1。
- TestCase类:测试用例的基类,包含
setUp
、tearDown
和run
方法。setUp
和tearDown
目前为空实现,run
方法负责测试的执行流程,包括调用setUp
、执行测试方法、处理异常以及调用tearDown
。 - TestResult类:用于统计测试运行次数和失败次数,并提供
summary
方法返回测试结果摘要。 - TestCaseTest类:用于测试
TestCase
和WasRun
类相关功能的测试用例类。setUp
方法初始化TestResult
对象,并创建WasRun
实例。testSetUp
方法运行测试并验证setUp
方法是否正确执行,即wasSetUp
是否为1。testRunning
方法运行测试并验证测试方法是否被正确执行,即wasRun
是否为1。
在if __name__ == "__main__"
块中,分别运行了testSetUp
和testRunning
两个测试用例,并打印测试结果摘要。通过这些代码示例,可以更直观地理解测试编写中的3A原则、setUp
方法的运用以及测试结果的统计等内容。
本章小结
回顾这部分内容,我们完成了以下重要任务:
- 明确了在测试编写中,简单性比运行时的性能更重要这一观点。这提醒开发者在追求测试效率的同时,不能忽视测试代码的简洁性和可读性,因为简单的测试代码更易于维护和理解。
- 测试并实现了setUp方法,用于测试前的准备工作。通过实际的代码编写和测试,我们掌握了如何利用setUp方法为测试设置初始条件,确保测试的顺利进行。
- 使用setUp方法简化了样板测试用例以及验证样板测试用例的测试用例。这不仅提高了测试代码的复用性,还减少了重复代码,使测试代码更加简洁和高效。
后续,我们还将继续探索测试框架中tearDown方法的运用,以及如何更好地实现多测试用例的运行和测试结果的详细报告等功能,进一步完善对xUnit测试框架的理解与应用,为软件开发的质量保障提供更强大的支持。