第18章 走进xUnit:测试驱动开发的关键工具
写在前面
这本书是我们老板推荐过的,我在《价值心法》的推荐书单里也看到了它。用了一段时间 Cursor 软件后,我突然思考,对于测试开发工程师来说,什么才更有价值呢?如何让 AI 工具更好地辅助自己写代码,或许优质的单元测试是一个切入点。 就我个人而言,这本书确实很有帮助。第一次读的时候,很多细节我都不太懂,但将书中内容应用到工作中后,我受益匪浅。比如面对一些让人抓狂的代码设计时,书里的方法能让我逐步深入理解代码的逻辑与设计。 作为一名测试开发工程师,我想把学习这本书的经验分享给大家,希望能给大家带来帮助。因为现在工作中大多使用 Python 代码,所以我把书中JAVA案例都用 Python 代码进行了改写 。
在软件开发领域,测试驱动开发(TDD)是保障代码质量的有效实践,xUnit作为TDD常用的测试框架家族,蕴含着丰富的逻辑与应用价值。接下来,我们通过更完整的代码示例深入剖析其工作原理。
测试驱动开发工具的独特性
利用测试工具自身运行测试来驱动开发,如同给自己实施精细的外科手术,这正是xUnit测试框架的特别之处。在缺乏现成完美框架的情况下,开发者需亲自验证每一步开发,确保代码符合预期。
代码示例
class WasRun:
def __init__(self, name):
self.wasRun = None
self.name = name
def setup(self):
self.wasRun = 0
print("setup")
def testMethod(self):
print("testMethod")
self.wasRun = 1
def teardown(self):
print("teardown")
class TestCase:
def __init__(self, name):
self.name = name
def setUp(self):
pass
def tearDown(self):
pass
def 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 result
class TestResult:
def __init__(self):
self.runCount = 0
self.errorCount = 0
def testStarted(self):
self.runCount = self.runCount + 1
def testFailed(self):
self.errorCount = self.errorCount + 1
def summary(self):
return "%d run, %d failed" % (self.runCount, self.errorCount)
class TestCaseTest(TestCase):
def setUp(self):
self.result = TestResult()
def testTemplateMethod(self):
test = WasRun("testMethod")
test.run(self.result)
assert ("setup testMethod teardown" == test.log)
def testResult(self):
test = WasRun("testMethod")
test.run(self.result)
assert ("1 run, 0 failed" == self.result.summary())
def testFailedResult(self):
test = WasRun("testMethod")
test.testMethod = lambda: 1 / 0
test.run(self.result)
assert ("1 run, 1 failed" == self.result.summary())
def testFailedResultFormatting(self):
result = TestResult()
result.testStarted()
result.testFailed()
assert ("1 run, 1 failed" == result.summary())
if __name__ == "__main__":
suite = TestCaseTest("testTemplateMethod")
result = suite.run()
print(result.summary())
代码逻辑推理
WasRun类
- 初始化:
__init__
方法设置wasRun
属性为None
,记录测试方法是否被运行,同时保存测试方法名称。 - setup方法:将
wasRun
设置为0,代表测试开始前的初始状态,并打印"setup",模拟测试前的准备工作。 - testMethod方法:打印"testMethod",代表测试方法的执行逻辑,并将
wasRun
设置为1,标识测试方法已被执行。 - teardown方法:打印"teardown",模拟测试后的清理工作。
TestCase类
- 初始化:
__init__
方法保存测试用例名称。 - setUp和tearDown方法:默认实现为空,可由子类重写以进行测试前准备和测试后清理。
- run方法:接收一个
TestResult
对象(若未传入则创建新的),先调用testStarted
方法标记测试开始,接着执行setUp
,然后尝试调用指定名称的测试方法,若方法执行中抛出异常则调用testFailed
记录失败,最后执行tearDown
,并返回TestResult
对象。
TestResult类
- 初始化:设置
runCount
(测试运行次数)为0,errorCount
(测试失败次数)为0。 - testStarted方法:每次测试开始时将
runCount
加1。 - testFailed方法:测试失败时将
errorCount
加1。 - summary方法:返回测试运行和失败次数的摘要信息。
TestCaseTest类
继承自TestCase
类,包含多个测试方法:
- testTemplateMethod:测试
TestCase
类的模板方法逻辑,创建WasRun
实例并运行测试,验证测试过程中的日志记录是否符合预期。 - testResult:验证
TestResult
类在正常测试情况下统计结果的正确性。 - testFailedResult:故意使测试方法抛出异常,验证
TestResult
类在测试失败时统计结果的正确性。 - testFailedResultFormatting:单独测试
TestResult
类在测试失败时摘要信息的格式化是否正确。
在if __name__ == "__main__"
块中,创建TestCaseTest
实例并运行测试,打印测试结果摘要。通过这些代码和逻辑,我们能清晰看到xUnit风格的测试框架如何组织和运行测试用例,以及处理测试结果。
已完成与待完成事项
在之前的讨论中,我们明确了完善的测试框架包含以下关键待办事项:
- Invoke setUp first:在测试执行前首先调用设置方法,用于初始化测试所需的环境、资源等,为测试的顺利进行做好准备。
- Invoke tearDown afterwards:在测试结束后调用清理方法,用于释放测试过程中占用的资源,如关闭文件、数据库连接等,确保测试环境恢复原状。
- Invoke tearDown even if the test method fails:即使测试方法执行失败,也要调用清理方法,保证无论测试结果如何,都能正确处理测试过程中产生的资源,维持测试环境的一致性和稳定性。
- Run multiple tests:具备运行多个测试的能力,能够将多个测试用例组织起来并依次执行,提高测试效率,满足项目中大量测试用例的执行需求。
- Report collected results:收集并报告测试结果,提供详细的测试信息,包括哪些测试通过、哪些失败,以及失败的原因等,帮助开发者快速定位和解决问题。
在当前的示例代码中,我们仅仅初步实现了部分基础功能:
- 创建测试用例类:定义了
TestCase
类,用于封装测试方法和相关的测试逻辑。 - 实现测试执行逻辑:在
TestCase
类中实现了run
方法,能够执行单个测试方法,并在执行前后预留了setUp
和tearDown
方法的调用位置,不过目前这两个方法为空实现。 - 统计测试结果:创建了
TestResult
类,用于统计测试运行的次数和失败的次数,能够生成简单的测试结果摘要。 - 测试功能验证:编写了
TestCaseTest
类,对TestCase
和TestResult
类的关键功能进行了简单的测试验证,确保基本的测试流程和结果统计功能正常。
下一章节,我们将聚焦于这些待完成事项,深入探讨如何在实际项目中进一步完善xUnit测试框架的应用,提升测试效率和质量。