一个完整的TDD演练案例(二)

简介: 一个完整的TDD演练案例(二)


说明:本讲义是我在ThoughtWorks作为咨询师时,为客户开展TDD Code Kata而编写。案例为Guess Number,案例需求来自当时的同事王瑜珩。当时,我们共同在ThoughtWorks的Zynx交付团队,为培养团队TDD能力进行训练时,引入了本案例。讲义中给出的代码问题则来自客户方的受训学员,可谓“真实的代码坏味道”。个人认为TDD不只是开发方法,还应该是设计方法,因此讲义中包含了诸多设计原理、思想和原则。

目标收益


  • 熟悉IDE快捷键;
  • 掌握TDD基本知识;
  • 识别代码坏味道,熟练运用重构手法;
  • 熟悉JUnit与Mockito框架;
  • 了解Google Guice框架;


在编写第二个测试时,由于测试样本与之前的测试完全不一样,之前的简单实现就不能满足新增的测试了。事实上,测试就是要去验证实现逻辑,这其中最重要的测试目标就是分支。不同的分支可能会返回不同的结果,如果我们根据分支来设计测试,就能有效保障实现的正确性。这称为“三角测试法”。

常见问题:

  • 没有将测试代码看做是代码的一部分。当编写多个测试方法时,没有及时重构;例如,应及时将game对象与actualAnswer对象提取为字段,以避免不必要的声明。
  • 直接暴露表达式,而未对表达式进行方法提取,以表达业务意义;
  • guess()方法过长;应该通过提取方法来改进代码的可读性;
  • Game类与Answer类的职责分配不合理,将Answer类设计为仅具有get()和set()的数据对象,而将判断数值是否正确、位置是否正确的逻辑分配给了Game。没有考虑get()和set()是否真正有必要;如果我们对guess()方法进行了方法提取,可以识别出代码的坏味道“Feature Envy”,即Game的方法用到的都是Answer的属性。这时,应该采用移动方法的重构手法对其进行重构。

开始第二个任务


我们选择的第二个任务为“随机生成答案”,这是一个独立的职责。编写测试类时,很容易驱动出AnswerGenerator类。关键在于,我们该如何编写单元测试来验证生成的结果。我们对结果的要求是:

  • 数字必须是0…9之间;
  • 产生的四个数字不能相同;

讨论:究竟由谁来承担“随机生成答案”的职责?


学员容易将此职责直接分配给Answer。然而,随机生成答案与创建一个答案适用于不同的场景,这对于Answer的调用者而言,并不友好。尤其对于只需要答案的场景,还需要无端地引入对随机数的依赖,显然是不合理的。


编写测试方法的过程与前相似,仍然按照Given-When-Then模式来编写(若测试方法比较简单,可以不遵循这一模式,但思考的过程却应该按照该模式)。

在编写then部分的测试时,可能出现疑问。


问题:如何验证生成的答案是否正确?

我们已经将答案建模为Answer,因此AnswerGenerator的generate()方法要返回的对象类型为Answer。那么,我们怎么知道返回的Answer对象是合法的呢?一种做法是获取Answer的属性,然后再进行验证。那么,为了测试的验证而暴露这些属性,是否适合?


要完成对答案正确性的验证,直接暴露答案的属性是不妥当的,至少目前没有获取答案属性的需求。我们的做法是定义一个验证方法。这是否仍然属于为测试而定义行为的做法呢?这个问题有点像鸡与鸡蛋的哲学问题。我们应该还原到设计,看看这种手法是否改善了设计,如此即可。毕竟,这种对答案正确性的校验,也可以说是业务逻辑的一种。

说明:在开始编写“检查输入是否合法”任务时,你会发现,这里所谓多余的验证,就会派上用场。

这个验证方法可以是单纯的返回true或者false,但从需求来看,这个返回结果并没有很好地展现验证要求:究竟是因为数字超出了范围,还是出现了相同的数字?我个人更倾向于用自定义异常来表示生成的答案违背了这两条规则。因此,我们可以为Answer定义一个validate()方法,以验证生成的Answer是否满足规则要求;如果不符合,就抛出对应的异常。


知识:JUnit中对异常的验证


随着JUnit版本的演化,先后提供了三种验证异常的机制。

  • 一种是传统的在测试代码中通过编写try... catch结合fail()方法进行验证。这种方法带来的问题是验证逻辑太繁琐。
  • 第二种方法是利用@Test的expected方法,通过指定异常类型值来验证。它的好处是简单直接,缺点是只能验证抛出异常的类型。
  • 第三种方法是利用ExpectedException Rule。Rule可以更灵活地验证异常,包括异常类型和异常消息。我们也可以通过定义派生自TypeSafeMatcher的Matcher类,来验证异常的更多信息。

问题:如何确定测试通过就意味着实现正确?

第二个任务看似简单,实则不然。原因在于这里产生了一个随机数。随机数带来了不确定性,它可能偶然地让测试通过了。也许,运行测试100次,前面的99次都通过了,最后一次失败,仍然视为失败。

生成随机数自然是调用Java的JDK。在单元测试环节中,倘若我们要测试的单元需要调用别的API,则在这个测试中,我们可以假定这个API是正确的。我们对Java JDK的正确性自然信心十足。那么,为何我们还要考虑测试的随机失败?这是因为在这个任务的测试中,我们测试的并非随机数的生成逻辑,而在于随机数的种子是否恰当,实现逻辑中是否判断了可能出现的错误数字?

由于生成随机数的逻辑并非确定无疑的,测试时我们就不能依赖于它。这正是Mock可以派上用场的时候。为此,我们需要将生成随机数的功能提取为类RandomIntGenerator,再注入到AnswerGenerator中。

public class AnswerGenerator {     
    private RandomIntGenerator randomIntGenerator;     
    public AnswerGenerator(RandomIntGenerator randomIntGenerator) {         
        this.randomIntGenerator = randomIntGenerator;     
    }
}

该类的实现调用了Java提供的Random类,但在测试时,我们却可以通过Mock它的行为,使得返回的结果变为确定的数字:

@Test(expected = OutOfRangeAnswerException.class)     
public void should_throw_OutOfRangeAnswerException_which_is_not_between_0_and_9() {         
    RandomIntGenerator randomIntGenerator = mock(RandomIntGenerator.class);         
    when(randomIntGenerator.nextInt()).thenReturn(1, 2, 3, 10);         
    AnswerGenerator answerGenerator = new AnswerGenerator(randomIntGenerator);
    answerGenerator.generate(); 
}


重构:组合Game与AnswerGenerator


在实现第一个任务时,我们定义的Game接受了Answer对象作为游戏的答案。现在,我们定义了AnswerGenerator用以生成符合条件的随机答案。我们当然可以在调用该对象的generate()方法生成答案后,再将该答案作为构造函数参数传递给Game对象。但更好的做法是直接将AnswerGenerator作为构造函数参数传递给Game,在其内部调用它的generate()方法。

相关文章
|
6月前
|
敏捷开发 数据管理 jenkins
探索自动化测试框架:从理论到实践
【7月更文挑战第25天】在软件开发的生命周期中,测试阶段扮演着至关重要的角色。随着敏捷开发方法的普及和持续集成/持续部署(CI/CD)的实践,自动化测试成为了确保软件质量和提高交付速度的关键工具。本文将深入探讨自动化测试框架的核心概念、设计原则及其在实际项目中的应用。我们将通过一个具体的案例研究,展示如何从零开始构建一个自动化测试框架,包括选择合适的测试工具、设计测试用例以及实现持续集成流程。文章旨在为读者提供一套完整的指南,帮助他们理解并实施有效的自动化测试策略。
47 6
|
6月前
|
测试技术 开发者 运维
开发与运维测试问题之单元测试过程如何解决
开发与运维测试问题之单元测试过程如何解决
|
8月前
|
算法 测试技术 开发者
测试驱动开发(TDD)实战:从理论到实践
【5月更文挑战第8天】TDD实战指南:先测试后开发,确保代码质量与可维护性。核心思想是编写测试用例→实现代码→验证→重构。优点包括提高代码质量、促进设计思考和增强可测试性。实战步骤包括编写独立、明确的测试用例,遵循最小可用原则编写代码,运行测试并分析失败原因,以及在验证通过后进行代码重构与优化。通过TDD,开发者能提升编程技能和项目成功率。
|
8月前
|
运维 测试技术 程序员
集成测试如何做?
集成测试如何做?
279 0
|
自然语言处理 算法 IDE
一个完整的TDD演练案例(一)
一个完整的TDD演练案例(一)
一个完整的TDD演练案例(一)
|
前端开发 JavaScript Java
TDD测试驱动开发案例【水货】
TDD测试驱动开发案例【水货】
|
测试技术
嵌入式软件测试笔记9 | 嵌入式软件测试中如何做好评审工作?
嵌入式软件测试笔记9 | 嵌入式软件测试中如何做好评审工作?
146 0
|
Java 程序员 Spring
一个完整的TDD演练案例(完)
一个完整的TDD演练案例(完)
|
存储 算法 Cloud Native
基于测试的质量守护:分层测试、测试自动化、单元测试 | 学习笔记
快速学习基于测试的质量守护:分层测试、测试自动化、单元测试
基于测试的质量守护:分层测试、测试自动化、单元测试 | 学习笔记
|
XML 设计模式 前端开发
一个完整的TDD演练案例(四)
一个完整的TDD演练案例(四)

热门文章

最新文章