跳转至

03 TDD演示(3):按测试策略重组测试

你好,我是徐昊。今天我们来继续进行命令行参数解析的TDD演示。

首先让我们回顾一下题目与需求与代码进度。如前所述,题目源自Bob大叔的 Clean Code 第十四章:

我们中的大多数人都不得不时不时地解析一下命令行参数。如果我们没有一个方便的工具,那么我们就简单地处理一下传入main函数的字符串数组。有很多开源工具可以完成这个任务,但它们可能并不能完全满足我们的要求。所以我们再写一个吧。

传递给程序的参数由标志和值组成。标志应该是一个字符,前面有一个减号。每个标志都应该有零个或多个与之相关的值。例如:

-l -p 8080 -d /usr/logs

“l”(日志)没有相关的值,它是一个布尔标志,如果存在则为true,不存在则为false。“p”(端口)有一个整数值,“d”(目录)有一个字符串值。标志后面如果存在多个值,则该标志表示一个列表:

-g this is a list -d 1 2 -3 5

"g"表示一个字符串列表[“this”, “is”, “a”, “list”],“d"标志表示一个整数列表[1, 2, -3, 5]。

如果参数中没有指定某个标志,那么解析器应该指定一个默认值。例如,false代表布尔值,0代表数字,”"代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。

确保你的代码是可扩展的,即如何增加新的数值类型是直接和明显的。

目前我们的代码结构如下图所示:

我们目前的任务列表为:

ArgsTest:
// sad path:
// TODO: -bool -l t / -l t f
// TODO: - int -p/ -p 8080 8081
// TODO: - string -d/ -d /usr/logs /usr/vars
// default value:
// TODO: - bool : false
// TODO: -int :0
// TODO: - string ""

调整任务列表

当我们罗列任务列表的时候,还没有进行重构,系统中也只有Args一个类。而经过重构之后,我们提取了OptionParser接口,以及与之对应的实现类:BooleanOptionParser和SingleValuedOptionParser。那么当再去构造测试的时候,就存在两个不同的选择:继续针对Args进行测试,或是直接对BooleanOptionParser进行测试。

代码分别如下所示:

@Test
public void should_not_accept_extra_argument_for_boolean_option() {
  TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, 
    () -> Args.parse(BooleanOption.class, "-l", "t"));
  assertEquals("l", e.getOption()); 
}

@Test
public void should_not_accept_extra_argument_for_boolean_option() {
    TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () ->
            new BooleanOptionParser().parse("-l", "t", option("l")));
    assertEquals("l", e.getOption());
}

在当前的架构下,这两个测试是等效的功能验证,但是它们的测试范围不同,在下图中,我用虚线边框圈定了它们的范围:

那么在这种情况下,我们可以选择粒度更小的测试,这样更有益于问题的定位。于是,我们可以修改任务列表,将剩余的任务分配到对应的组件上去:

BooleanOptionParserTest: 
// sad path:
// TODO: -bool -l t / -l t f
// default:
// TODO: - bool : false

SingleValuedOptionParserTest:
// sad path:
// TODO: - int -p/ -p 8080 8081
// TODO: - string -d/ -d /usr/logs /usr/vars
// default value:
// TODO: -int :0
// TODO: - string ""

现在让我们进入红/绿循环:

类似的,根据任务列表,完成SingleValuedOptionParser的功能:

按照测试策略重组测试

在这个红/绿环节中,我们发现在整数类型和字符串类型的异常场景中,差异仅仅在于如何构造SingleValuedOptionParser:

new SingleValuedOptionParser(0, Integer:parseInt)
new SingleValuedOptionParser("", String::valueOf)

也就是说,仅仅是测试代码的差别,而被测试的代码则没有任何区别。我们按照任务列表,再构造其他场景的测试,也仅仅是不同测试数据的重复而已。所以将剩余任务从列表中取消就好了。

在当前的代码中,还遗存着一些重构前的测试。对比经过重构之后新写的测试,就会发现对于类似的功能,我们测试的出发点和测试的范围都有不同,这是一种坏味道。我们需要对测试进行重构,以消除这些不一致:

在继续完成其他功能之前,我们可以快速审查一下代码,可以显而易见地发现几个明显的Bug,那么我们可以通过一系列红/绿环节来修复它们:

好了,到此为止,我们得到了一个颇为健壮的代码,以及清晰、可扩展的代码结构。

小结

在这节课中,我们展示了红/绿/重构循环是如何与任务列表互动,任务列表又是怎样持续指导我们进行测试驱动开发的。让我们回想一下最开始构想的任务列表:

// TODO: boolean -l
// TODO: int -p 8080
// TODO: string -d /usr/logs
// TODO: example 1
// sad path:
// TODO: -bool -l t / -l t f
// TODO: - int -p/ -p 8080 8081
// TODO: - string -d/ -d /usr/logs /usr/vars
// default value:
// TODO: - bool : false
// TODO: -int :0
// TODO: - string ""

我们真正的开发过程是这样的,先按照任务列表完成了一块功能点:

// TODO: boolean -l 
// TODO: int -p 8080
// TODO: string -d /usr/logs
// TODO: example 1

发现了坏味道,开始重构。通过重构引入了新的组件,改变了架构。于是剩余的任务列表改为:

BooleanOptionParserTest: 
// sad path:
// TODO: -bool -l t / -l t f
// default:
// TODO: - bool : fals

SingleValuedOptionParserTest:
// sad path:
// TODO: - int -p/ -p 8080 8081
// default value:
// TODO: -int :0

陆续完成这些任务,发现不一致的测试策略,重组测试。然后进行代码审查,发现了几个缺陷,于是剩余任务列表变为(请重点关注列表的变化):

ArgsTest:
// TODO:无标注的参数
// TODO:不支持的类型

SingleValuedOptionParserTest:
// TODO: 错误的数值格式

不难发现,任务列表是一个随代码结构(重构)、测试策略(在哪个范围内测试)、代码实现情况(存在哪些缺陷)等因素而动态调整的列表。它的内容体现了我们最新的认知,它的变化记录了我们认知改变的过程。

下节课,我们将继续完成命令行列表标志的功能。我们会重复任务分解与红/绿/重构循环。请注意,对于列表标志的任务分解与我们已完成的功能有何不同。

思考题

请根据当前代码结构,对列表标志功能进行任务分解。

如果你在学习过程中还有什么问题或想法,欢迎加入读者交流群。最后,也欢迎把你学习这节课的代码与体会分享在留言区,我们下节课再见!

精选留言(15)
  • 🐑 👍(7) 💬(0)

    大家好~我是TDD这门课的编辑辰洋~ 🎐我来友情提示一下下~ 01-04是视频演示,好对TDD有个感性的认识。 05-10是对演示的复盘,同时也会讲解TDD的相关知识点。比如测试的基本结构,TDD中的测试是啥~ 所以,如果你在01-04的操作卡壳了,可以从05开始学,看看5-10,这才是重点哇。看完再回头去看01-04~

    2022-03-22

  • wenming 👍(9) 💬(1)

    老师,任务列表里面的 Corner case 部分,除了代码审查发现,还有没有其他办法能够避免遗漏导致 BUG 呢?

    2022-03-21

  • 临风 👍(17) 💬(1)

    测试代码也是需要重构的,之前一直没有意识到这点,觉得就算有点冗余也没关系。但实际上,只要是多余的代码就意味着团队的负债,会增加编译成本、跑用例的时间成本(这两项影响不大),更重要的是影响理解的成本,好的测试,就应该通过测试用例就能清晰的理解业务逻辑,而不是一行一行的去看代码。 学习了几讲后,越发觉得TDD其实是一种内功修炼,无论你的水平是多少,都是可以通过TDD不断精进,你对语言特性、设计模式、重构手法等等基本功,还有你对业务的理解,都通过一个个的用例、一次次重构体现出来。 TDD是一种对做事方法的极致拆分,一次只做一件事,思考业务逻辑时就不考虑实现和代码坏味道;编写业务代码时,也仅考虑能通过用例的逻辑;而重构时,也是不能改变原来的代码逻辑的。通过一个个极小粒度的操作,实现最终整体的协调,有种艺术的美感。

    2022-03-21

  • Gojustforfun 👍(1) 💬(1)

    Go演示git提交记录: https://github.com/longyue0521/TDD-In-Go/commits/args Commits on Mar 28, 2022 ~ Commits on Mar 26, 2022之间的内容与本篇文章对应. 采用baby step每步都有提交记录可以对比学习. 如果觉得本项目对你有帮助,欢迎留言、star

    2022-03-29

  • Geek_7c4953 👍(0) 💬(1)

    这节课的视频看过来,感觉代码变得难以阅读了。 我觉得原因有两点: 1.因为对坏味道的延迟处理,让坏味道影响了代码的可读性。 2.因为对逻辑的构建是点式的而非线性的,导致思维在几个点之间跳跃而非循序渐进的,造成思维处在“先回忆再思考”的循环中。 不过也有可能是因为不是第一人称写代码,所以思维并不是完全跟上,不知道老师对这个问题怎么看。

    2022-04-12

  • webmin 👍(8) 💬(0)

    三课观看下来有一种和老师结对的感觉,忽然明白了结对编程的相互学习过程比结果更最要,以往看到的代码包括开源项目的都已经是阶段性的结果,这个阶段结果的产生其实中间还一些子过程你是看不到的,就算你有设计文档和实现代码但是中间过程是缺失的,且这些中间过程也是无法从设计文档和结果代码反推出来的。中间的这些渐进过程才是内功心法。

    2022-03-23

  • 含低调 👍(3) 💬(2)

    真是跪着看完的

    2022-12-27

  • 6点无痛早起学习的和尚 👍(2) 💬(0)

    2024年01月26日09:28:47 真的是跪着看完的,太强了,有很多不足的知识

    2024-01-26

  • Geek_7c0961 👍(2) 💬(0)

    这课最大的痛点之一是用了太多的java 专用技法,对c++用户特别不友好

    2023-02-01

  • aoe 👍(2) 💬(2)

    03 课 学习笔记 http://wyyl1.com/post/19/03/ 源码 https://github.com/wyyl1/geektime-tdd/tree/branch-03 摘要: 1. 测试代码需要重构 2. 提前将 Option 对象提取出 flag 的代码在 bad path 翻车了 3. 视频中代码的两个坏味道,期中之一:if 没有 “{}”。苹果公司的“GoTo Fail 漏洞”,就是因为没有括号引起的。 内容摘自:范学雷老师的专栏 极客时间 | 代码精进之路 | 02 | 把错误关在笼子里的五道关卡

    2022-03-23

  • 老衲 👍(1) 💬(0)

    这一节跟上一节的内容是不是少了对TooManyArgumentsException 的封装描述?

    2022-07-21

  • 子夜枯灯 👍(1) 💬(0)

    代学习源码: https://github.com/ziyekudeng/my-geektime-tdd.git

    2022-05-23

  • 奇小易 👍(1) 💬(0)

    2W2H笔记 What Q: 什么是按测试策略重组测试? A: 视频中的操作实际上就是把ArgsTest中的部分测试挪到了BooleanOptionParserTest中。 本质上就是让所有测试都能放到合适的地方。也就是按照测试策略(哪个测试方法放哪里)重组了测试。 Q:任务列表和红/绿/重构循环之间是如何相互影响的? A: 第一课中,分解出第一版任务列表,完成了三种简单类型的解析的Happy Path。 第二课中,识别现有代码的坏味道,并使用重构将其消除。此时代码结构已经被调整。 第三课中, 由于代码结构的调整,故任务列表也随之进行调整。 继续完成三种简单类型解析的sad path和default value。 发现测试代码中存在坏味道,对测试代码进行重组。 完成这些操作之后,对当前代码进行代码审查,发现有些新的任务需要做。 此时任务列表也被更新。 How Q: 在同一个代码结构下,有两个测试的验证功能是相同的,而它们的测试范围却不同。 此时该选择哪一种? A: 选择范围更小的测试,粒度更小能够更快速的定位问题。

    2022-04-07

  • 苏莉斌 👍(0) 💬(0)

    想问下老师的代码有在哪里可以共享下吗

    2024-03-03

  • Geek_32ff72 👍(0) 💬(0)

    如果port参数不是字符串,parseInt就会失败,这个是不是没有考虑呢?

    2023-01-10