写在前面
这本书是我们老板推荐过的,我在《价值心法》的推荐书单里也看到了它。用了一段时间 Cursor 软件后,我突然思考,对于测试开发工程师来说,什么才更有价值呢?如何让 AI 工具更好地辅助自己写代码,或许优质的单元测试是一个切入点。 就我个人而言,这本书确实很有帮助。第一次读的时候,很多细节我都不太懂,但将书中内容应用到工作中后,我受益匪浅。比如面对一些让人抓狂的代码设计时,书里的方法能让我逐步深入理解代码的逻辑与设计。 作为一名测试开发工程师,我想把学习这本书的经验分享给大家,希望能给大家带来帮助。因为现在工作中大多使用 Python 代码,所以我把书中JAVA案例都用 Python 代码进行了改写 。
问题背景说明
在完成货币类的一系列开发与优化后,对整个开发过程进行回顾和总结具有重要意义。通过回顾,可以总结经验教训,发现开发过程中的优点和不足之处,为未来的项目开发提供参考,同时也有助于加深对测试驱动开发流程和方法的理解和掌握,提高开发者的技术水平和开发效率。
完成货币类的一系列开发与优化后,对整个过程进行回顾总结,有助于加深对Python TDD的理解,为后续开发积累经验。
17.1 思路说明
回顾从简单的乘法功能测试到复杂的多币种货币运算实现的过程,分析测试驱动开发的流程、遇到的问题及解决方法,总结代码优化和设计的思路。
17.2 测试代码(综合回顾测试)
import unittestclass Bank:def __init__(self):# 存储货币兑换汇率的字典,键是 (from_currency, to_currency) 元组,值是汇率self.rates = {}def addRate(self, from_currency, to_currency, rate):# 添加一种货币到另一种货币的汇率self.rates[(from_currency, to_currency)] = ratedef rate(self, from_currency, to_currency):# 获取从 from_currency 到 to_currency 的汇率# 如果是相同货币,汇率为 1if from_currency == to_currency:return 1# 尝试从 rates 字典中获取汇率,如果没有则默认为 1return self.rates.get((from_currency, to_currency), 1)def reduce(self, source, to):# 将表达式(如 Sum 或 Money)转换为指定货币的 Money 对象if isinstance(source, Sum):# 如果是 Sum 类型,将加数和被加数都转换为目标货币,然后求和amount = source.augend.reduce(self, to).amount + source.addend.reduce(self, to).amountreturn Money(amount, to)elif isinstance(source, Money):# 如果是 Money 类型,根据汇率进行转换rate = self.rate(source.currency, to)return Money(source.amount / rate, to)class Expression:def reduce(self, bank, to):# 抽象方法,用于将表达式转换为指定货币的 Money 对象passclass Money(Expression):def __init__(self, amount, currency):# 金额self.amount = amount# 货币类型self.currency = currencydef plus(self, addend):# 实现加法操作,返回一个 Sum 对象return Sum(self, addend)def times(self, multiplier):# 实现乘法操作,返回一个新的 Money 对象,金额乘以乘数return Money(self.amount * multiplier, self.currency)def reduce(self, bank, to):# 将自身转换为指定货币的 Money 对象# 调用 bank 的 rate 方法获取汇率rate = bank.rate(self.currency, to)return Money(self.amount / rate, to)class Sum(Expression):def __init__(self, augend, addend):# 加数self.augend = augend# 被加数self.addend = addenddef reduce(self, bank, to):# 将 Sum 中的加数和被加数转换为指定货币,然后求和amount = self.augend.reduce(bank, to).amount + self.addend.reduce(bank, to).amountreturn Money(amount, to)class Dollar(Money):def __init__(self, amount):# 调用父类 Money 的构造函数,将货币类型设置为 USDsuper().__init__(amount, "USD")class Franc(Money):def __init__(self, amount):# 调用父类 Money 的构造函数,将货币类型设置为 CHFsuper().__init__(amount, "CHF")class TestAllCurrencyOperations(unittest.TestCase):def test_mixed_addition(self):bank = Bank()# 添加瑞士法郎到美元的汇率为 2bank.addRate("CHF", "USD", 2)# 创建 5 美元的对象five_dollars = Dollar(5)# 创建 10 瑞士法郎的对象ten_francs = Franc(10)# 将 5 美元和 10 瑞士法郎相加并转换为美元result = five_dollars.plus(ten_francs).reduce(bank, "USD")# 验证结果是否等于 10 美元self.assertEqual(result.amount, 10)# 验证结果的货币类型是否为美元self.assertEqual(result.currency, "USD")def test_dollar_multiplication(self):# 创建 5 美元的对象five_dollars = Dollar(5)# 将 5 美元乘以 3result = five_dollars.times(3)# 验证结果是否等于 15 美元self.assertEqual(result.amount, 15)# 验证结果的货币类型是否为美元self.assertEqual(result.currency, "USD")if __name__ == '__main__':unittest.main()
17.3 源码说明
这段综合测试代码涵盖了多币种加法运算和货币乘法运算的测试用例。通过回顾整个开发过程,我们从最初的简单功能逐步实现了复杂的货币运算系统。在这个过程中,不断地编写测试用例、实现功能代码、优化代码结构,遵循TDD的流程解决了各种问题,提高了代码的质量和可维护性。总结这些经验,能帮助我们在未来的Python开发中更好地应用TDD方法。
TDD过程中的关键要点回顾
在讲授和实践TDD的过程中,有几个关键要点反复出现,这些要点对于理解和应用TDD至关重要。
- 让测试工作的方式
1.1 伪实现:在TDD流程中,当编写完测试用例后,为了快速验证测试框架是否正常工作,以及测试用例是否能够按照预期运行,我们会采用伪实现的方式。这是一种临时性的代码实现,它并不追求功能的完整性和正确性,只是简单地让测试能够通过。例如,在开发货币类的乘法功能时,最初的times
方法可能只是返回一个固定值,像这样:
class Dollar:def __init__(self, amount):self.amount = amountdef times(self, multiplier):return 0 # 伪实现,仅为使测试通过
通过这种方式,我们可以快速检查测试环境和测试用例的基本逻辑是否正确。如果此时测试能够顺利通过,说明测试框架和测试用例的基本设置没有问题;如果测试失败,我们可以先排查测试相关的问题,而不是纠结于功能代码的实现细节。一旦确认测试环节无误,我们再逐步完善功能代码,将伪实现替换为真正的功能实现。
1.2 三角测量:三角测量是一种通过多个类似的测试用例来逐步明确需求和实现通用解决方案的方法。当我们只有一个测试用例时,可能会编写一个仅适用于该特定情况的代码实现,这可能导致代码缺乏通用性。而三角测量通过增加更多相似但又有细微差异的测试用例,帮助我们发现代码中的共性和变化点,从而设计出更通用的解决方案。例如,在测试Dollar
对象的乘法方法times
时,我们最初可能只有一个测试用例:
def test_dollar_multiplication():five = Dollar(5)result = five.times(2)assert result == 10
此时,times
方法的实现可能只是针对5 * 2
这个特定情况编写的。为了确保times
方法在各种输入下都能正确工作,我们可以增加更多测试用例:
def test_dollar_multiplication():five = Dollar(5)result = five.times(2)assert result == 10result = five.times(3)assert result == 15ten = Dollar(10)result = ten.times(4)assert result == 40
通过这些不同输入的测试用例,我们可以发现乘法运算的通用规律,进而实现一个能够适用于各种输入的times
方法,避免返回常量值,使代码更具通用性和健壮性。
1.3 显式实现:显式实现是TDD过程的最终阶段,在这个阶段,我们根据之前编写的测试用例和通过三角测量明确的需求,实现完整、正确且符合业务逻辑的功能代码。此时的代码不仅要满足所有测试用例的要求,还要考虑代码的性能、可读性、可维护性等方面。例如,在货币类开发中,经过伪实现和三角测量阶段后,我们最终实现的times
方法可能是这样的:
class Dollar:def __init__(self, amount):self.amount = amountdef times(self, multiplier):return self.amount * multiplier
这个实现是基于前面的测试和分析,明确了乘法运算的具体需求后完成的,它能够正确处理各种输入情况,并且符合货币金额乘法运算的实际逻辑。
-
消除重复:去掉测试代码和程序代码间的重复部分,是推进设计和开发的重要手段。在TDD开发过程中,随着功能的增加和代码量的增长,很容易出现重复代码。这些重复代码可能存在于测试用例中,也可能在功能代码中,或者在两者之间。重复代码不仅增加了代码量,还会使维护变得困难,因为一处修改可能需要在多个地方同步进行。通过重构,我们可以提取重复的代码片段,将其封装成独立的函数、方法或类,使代码更加简洁、易维护。例如,在测试不同货币类的乘法和加法运算时,如果多个测试用例中都有创建货币对象和进行基本断言的重复代码,我们可以将这些重复部分提取出来,封装成一个辅助函数或测试基类中的方法,减少代码冗余。
-
测试的控制力:当开发过程中遇到不确定因素时,要加强测试的控制力,如同在路面湿滑时增大摩擦力;而当情况明朗时,则可以加速前进,提高开发效率。在TDD中,测试的控制力体现在对测试用例的编写和执行上。当开发过程中遇到新的功能需求、复杂的业务逻辑或不确定的技术方案时,我们需要编写更多详细的测试用例,覆盖各种可能的情况,确保代码在各种条件下都能正确运行。这就好比在路面湿滑时,我们需要小心翼翼地控制车辆,增加摩擦力以确保安全行驶。例如,在实现多币种货币运算时,由于涉及汇率转换、不同货币对象的组合等复杂情况,我们需要编写大量测试用例来验证各种情况下的运算结果是否正确。而当我们对某个功能已经非常熟悉,代码实现也相对稳定时,我们可以适当减少测试用例的数量,或者采用更高效的测试策略,提高开发效率,就像在路况良好时可以适当加速行驶一样。但需要注意的是,即使在情况明朗时,也不能完全忽视测试,仍然要保证代码的质量和正确性。