徐昊 · TDD 项目实战 70 讲
徐昊
Thoughtworks 中国区 CTO
18159 人已学习
新⼈⾸单¥98
登录后,你可以任选4讲全文学习
课程目录
已完结/共 88 讲
实战项目二|RESTful开发框架:依赖注入容器 (24讲)
实战项目三|RESTful Web Services (44讲)
徐昊 · TDD 项目实战 70 讲
15
15
1.0x
00:00/06:22
登录|注册

01|TDD演示(1):任务分解法与整体工作流程

讲述:徐昊大小:5.82M时长:06:22
你好,我是徐昊。今天我们来聊聊测试驱动开发(Test-Driven Development,TDD)。
测试驱动开发,顾名思义,就是将软件需求转化为一组自动化测试,然后再根据测试描绘的场景,逐步实现软件功能的开发方法。
在正式开始学习 TDD 之前,我想通过四节课的时间,来演示如何通过 TDD 的方式完成一段完整的功能,让你对 TDD 的做法有个感性的认识。毕竟,我想很多人对 TDD 心存质疑,最主要还是因为不光没吃过猪肉,其实也没见过猪跑吧。

TDD 的基本原则

为了让我的演示更有针对性,有些基本原则你需要先了解一下。TDD 的创始人 Kent Beck,在他的传世大作 Test-Driven Development by Example 的开篇中给出了 TDD 的基本原则:
当且仅当存在失败的自动化测试时,才开始编写生产代码;
消除重复。
不过在今时今日,我认为第二条应该改为“消除坏味道(Bad Smell)”。毕竟重复仅仅是一种坏味道,还有很多不是重复的坏味道。
那么根据 TDD 的基本原则,Kent Beck 将开发工作分成了三步,也就是后世广为流传的测试驱动开发咒语——红 / 绿 / 重构(Red/Green/Refactoring):
红:编写一个失败的小测试,甚至可以是无法编译的测试;
绿:让这个测试快速通过,甚至不惜犯下任何罪恶;
重构:消除上一步中产生的所有重复(坏味道)。
然而红 / 绿 / 重构循环仅仅关注单个测试这个层面,它没有回答测试从何而来。于是很多尝试采用 TDD 的人都卡在了第零步:我该写哪些测试?于是在 2006 年前后我总结了任务分解法,将任务列表作为 TDD 的核心要素。
任务分解法的步骤如下:
大致构思软件被使用的方式,把握对外接口的方向;
大致构思功能的实现方式,划分所需的组件(Component)以及组件间的关系(所谓的架构)。当然,如果没思路,也可以不划分;
根据需求的功能描述拆分功能点,功能点要考虑正确路径(Happy Path)和边界条件(Sad Path);
依照组件以及组件间的关系,将功能拆分到对应组件;
针对拆分的结果编写测试,进入红 / 绿 / 重构循环。
那么 TDD 的整体工作流程如下图所示:
请花几分钟仔细记忆这个流程,它将会在后续所有示例题目中反复出现。

命令行参数解析

接下来,我会通过 TDD 来实现命令行参数解析的功能。这个练习源自 Robert C. Martin 的 *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 代表数字,”"代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。
 
确保你的代码是可扩展的,即如何增加新的数值类型是直接和明显的。
那么接下来,就让我们结合任务分解法使用 TDD 来完成这个需求。

API 构思与组件划分

首先我们需要考虑,别人将以何种方式使用这段代码,也就是这段代码的整体对外接口部分。我们可以通过写测试的方式,来感受 API 的友好程度。
00:00 / 00:00
    1.0x
    • 2.0x
    • 1.5x
    • 1.25x
    • 1.0x
    • 0.75x
    • 0.5x
    网页全屏
    全屏
    00:00
    在确定了 API 的形式之后,我们需要大致构思如何实现这个功能:
    00:00 / 00:00
      1.0x
      • 2.0x
      • 1.5x
      • 1.25x
      • 1.0x
      • 0.75x
      • 0.5x
      网页全屏
      全屏
      00:00

      功能分解与任务列表

      在 API 与实现方式有了方向之后,我们就可以根据需求的描述对功能进行分解了。这里可以先不求全面,有个大致的范围即可:
      00:00 / 00:00
        1.0x
        • 2.0x
        • 1.5x
        • 1.25x
        • 1.0x
        • 0.75x
        • 0.5x
        网页全屏
        全屏
        00:00

        红 / 绿循环

        那么先让我们选择最简单的任务,并通过红绿循环实现它。注意其中绿的环节中,我们是如何“不惜犯下任何罪恶”的!
        00:00 / 00:00
          1.0x
          • 2.0x
          • 1.5x
          • 1.25x
          • 1.0x
          • 0.75x
          • 0.5x
          网页全屏
          全屏
          00:00
          之后的两个任务,也要以同样的方式进行:
          00:00 / 00:00
            1.0x
            • 2.0x
            • 1.5x
            • 1.25x
            • 1.0x
            • 0.75x
            • 0.5x
            网页全屏
            全屏
            00:00
            到此为止,我们已经完成题目中要求的第一个功能点了:
            00:00 / 00:00
              1.0x
              • 2.0x
              • 1.5x
              • 1.25x
              • 1.0x
              • 0.75x
              • 0.5x
              网页全屏
              全屏
              00:00

              小结

              至此为止,我们实现了第一个主要功能:支持由三个不同类型组合而成的命令行参数的解析。
              可以看到,我们在进入 TDD 的红 / 绿 / 重构循环之前做了许多准备工作。我们先花费 2~3 分钟设计了 API,2~3 分钟构思了实现策略,然后在任务分解上花费了更多的时间,差不多有 5 分钟。到目前为止,编码的时间大约是 15 分钟。
              这并不像很多原教旨主义 TDD 实践者所推崇的那样,完全依赖重构而不去做设计。然而以我二十年来实践 TDD 的经验来看,理解需求,并通过测试构成高效的节奏,是有效实施 TDD 的前提。特别是在有其他团队成员的情况下(结对或项目组),更需要如此。希望你能从今天起,更加注重“TDD 的准备工作”。
              下节课,我们将在这段代码的基础上开始重构,然后再逐步完成后续的开发。

              思考题

              如果在思考实现策略的时候,我们选择了其他的实现方式,那么任务分解会有什么不同?
              如果你在学习过程中还有什么问题或想法,欢迎加入读者交流群。最后,也欢迎把你学习这节课的代码与体会分享在留言区,我们下节课再见!
              确认放弃笔记?
              放弃后所记笔记将不保留。
              新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
              批量公开的笔记不会为你同步至部落
              公开
              同步至部落
              取消
              完成
              0/2000
              荧光笔
              直线
              曲线
              笔记
              复制
              AI
              • 深入了解
              • 翻译
                • 英语
                • 中文简体
                • 中文繁体
                • 法语
                • 德语
                • 日语
                • 韩语
                • 俄语
                • 西班牙语
                • 阿拉伯语
              • 解释
              • 总结

              徐昊在文章中介绍了测试驱动开发(TDD)的基本原则和工作流程。他强调了TDD的红/绿/重构循环,并提出了任务分解法作为TDD的核心要素。他通过一个命令行参数解析的例子演示了如何使用TDD完成一个功能的开发过程,包括API构思、功能分解、红/绿循环等步骤。他强调了在进入TDD的红/绿/重构循环之前做准备工作的重要性。文章内容深入浅出,适合读者快速了解TDD的基本原则和实践方法。

              2022-03-1644人觉得很赞给文章提建议

              仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
              《徐昊 · TDD 项目实战 70 讲》
              新⼈⾸单¥98
              立即购买
              unpreview
              登录 后留言

              全部留言(38)

              • 最新
              • 精选
              • 🐑
                置顶
                TDD专栏福利大合集: 1、打卡赢好礼(4月23日-5月10日):正在进行中,学习专栏第1-10讲并在留言区打卡,结束后奖励; 2、代码亲手评(5月底):预计打卡结束后启动,完成前10讲的打卡,即可提交代码练习作业,徐昊老师会亲自点评; 3、线上带你练:根据专栏更新节奏和老师时间安排确定,徐昊老师会线上带四个同学手把手地改代码,敬请期待! 具体活动介绍见 👉 http://gk.link/a/11jPi
                2022-04-28
              • 🐑
                置顶
                大家好~我是TDD这门课的编辑辰洋~ 🎐我来友情提示一下下~ 01-04是视频演示,好对TDD有个感性的认识。 05-10是对演示的复盘,同时也会讲解TDD的相关知识点。比如测试的基本结构,TDD中的测试是啥~ 所以,如果你在01-04的操作卡壳了,可以从05开始学,看看5-10,这才是重点哇!看完再回头去看01-04~
                2022-03-22
                2
                4
              • Gojustforfun
                置顶
                Go语言实现: https://github.com/longyue0521/TDD-In-Go/tree/args 提交历史记录: https://github.com/longyue0521/TDD-In-Go/commits/args (用vscode对比提交记录看更方便) 1. TDD其实是三项已有技术的重组 —— 先大概设计,再落地测试,再重构出最终代码 1) 设计能力:软件设计原则/思想/模式 2) 测试能力:测试技术/方法/工具 3) 重构能力:代码坏味道,重构方法/工具 2. 需求拆分一般有两种方式 1) 任务列表 —— 从无到有实现各个功能点,从内到外,比如实现Option和ListOption等 2) 测试列表 —— 通过所有测试即表示实现功能,Outside-in,从外部API一点一点向内推 希望老师多提供案例,带领大家多多练习以提升需求拆分这项能力——解决测试从哪来的问题 3. 为什么一定要先看到红灯? 1) 测试的正确性如何保证?你不可能再为测试写测试,你需要看到测试以你预期的方式失败! 2) 如果你没看到红灯,要么测试有问题,要么代码已提前实现(推荐把代码改错,强制看到红灯) 3) 红灯表示“缺少功能/实现错误”,“以终为始”失败测试就是那个“终”帮助确定产品代码缺少当前失败测试所捕获的“功能”. 4) 此阶段的目标:确定“终”后需努力不落空! 4. 为什么一定要快速看到绿灯? 1) 关注点分离,“终”已经由失败的测试确定,代码只要通过“终”即可. 2) 代码硬编码,僵化,设计不好等问题,要么是“终”不够多,要么是重构阶段的锅 3) 此阶段的目标:用尽可能简洁的代码使当前所有测试通过! 5. 为什么一定要重构? 1) 好的代码使重构出来的,这里指的是代码的实现,API一般是提前构思好的,偶有部分返工. 2) 识别代码坏味道,熟练运用重构方法/技巧/工具及软件设计思想/原则/模式等驱动出最终产品代码和测试代码 3) 强调:测试代码也需要重构! 4) 每次重构都要运行所有测试,确保绿灯!一旦红灯,回退到绿灯再重构! 5) 此阶段的目标:弥补为了快速看到绿灯所犯的过错! 实践TDD就像玩游戏,进入下一个关卡(红灯/绿灯/重构)前先保存进度(git),挂了就重来!

                作者回复: good job

                2022-03-19
                2
                24
              • Jxin
                置顶
                总结 1.分离关注点,实现的时候只关注一个原子功能点的实现,不关心重构。(不纠结坏味道) 2.重构的时只看红绿过不过,不关心功能实现。(不需要顾虑是否改坏了) 疑问: 1.当前例子可以拆分成一个个原子功能,彼此没依赖。但有些时候功能与功能间是会嵌套的,该把他们拆开还是合并成一个去看?拆分原子功能(测试列表)的粒度是个难点。 2.实际工作中,我们伴随的可能是大量需要mock的接口,又该如何平衡?

                作者回复: 项目二 更复杂的例子

                2022-03-18
                2
              • 李圣悦
                java不太懂,tdd有不好的地方吗?往自己做的项目上套,大多业务相关的需要复杂环境,简单的获取cpu占用率,这种的如何自动化验证结果正确?如果更加复杂无法简单构造环境的呢?

                作者回复: 建模 分成根据数据进行操作 和按照实际情况生产数据两部分

                2022-04-22
                2
              • 阿崔cxr
                红 / 绿循环这一集里 我看是先写了一个伪实现,然后通过取反的测试来驱动出真正的实现 我自己的做法是按照 TPP 的步骤,一开始也是伪实现,不过因为我知道他是错误的,所以直接在这一个测试的基础上就直接把代码实现成正确的了, 没有做取反的测试。

                作者回复: 任务划分巧妙的话 可以避免triangel,不然重组测试的时候还麻烦

                2022-03-16
                2
                2
              • 冯俊晨
                作为Java小白,从0摸索配置。 Idea IntelliJ -> 创建新项目-> Java / Grovy / Kotlin -> start with sample code;等待编译完成,就形成了第一个视频里代码框架

                编辑回复: 加油!从0摸索,不容易👍

                2022-05-25
                1
              • 能用一个项目实战的方式吗? 比如真实的项目需求,从需求分解到任务的分解,到测试的目标, 大家都说这种参数的例子,总是有点教科书的感觉

                编辑回复: 到第12讲,后面三个项目都是实战项目

                2022-03-28
                1
              • 文经
                徐老师,我对Java不了解,通过注解编写测试用例隐藏了太多实现的细节,也多细节没看懂。能不能加个餐,介绍一下Java 通过注解编写的原理。

                作者回复: 可以找找其他资料

                2022-03-23
                1
              • aoe
                两天前看完视频,感觉很容易,当自信满满的开始练习时,完全不像视频中行云流水,而是一步一个砍。最后跟着视频写代码,写到解析多参数时感觉像在 LeetCode 上做算法题。以下是解题思路(最终还是跟着老师的视频敲代码。实现第一个布尔类型的时候用 Stream 求解,结果到第二个求整数的时候发现还是 List 好用,又换了回去) TDD 模板 1. API 构思与组件划分 首先我们需要考虑,**别人将以何种方式使用这段代码**,也就是这段代码的整体对外接口部分。 我们可以通过写测试的方式,来感受 API 的友好程度 2. 在确定了 API 的形式之后,我们需要大致构思如何实现这个功能 3. 功能分解与任务列表 ⭐️⭐️⭐️⭐️⭐️(TDD 核心之一) 在 API 与实现方式有了方向之后,我们就可以根据需求的描述对功能进行分解了。 这里可以先不求全面,有个大致的范围即可 4. 红 / 绿循环 那么先让我们选择最简单的任务,并通过红绿循环实现它 功能分析:解析空格分割的字符串 示例:-l -p 8080 -d /usr/logs -g this is a list -d 1 2 -3 5 功能 - 减号后的字符代表一个功能符号 - l - 功能:日志,没有相关的值 - 类型:布尔 - 描述:存在=true;不存在=false - p - 功能:端口 - 类型:整数 - 描述:有一个整数值 - d - 功能:目录 - 类型:字符串 - 描述有一个字符串 - g - 功能:一个字符串列表 - 类型:字符串 - d - 功能:表示一个整数列表 - 类型:整数组成的字符串 - 如果参数中没有指定某个标志,那么解析器应该指定一个默认值 - 布尔:false - 整数:0 - 列表:[] - 如果给出的参数与模式不匹配,给出友好提示 - 简要错误信息 - 告知错误原因 三种实现方式 1. -l -p 8080 -d /usr/logs 一次解析整个字符串解析(难度大) 2. [-l], [-p, 8080], [-d, /usr/logs] 按功能划分,处理特定数组(简单)💡 课程中选取了最简单的方案实现 3. {-l:[], -p:[8080], -d:[/usr/logs]} 按功能划分,从Map中取出相应的值(比数组复杂) 方式 2 具体实现 单个功能 布尔类型:-l 单整数类型:-p 8080 单个连续字符串类型:-d /usr/logs 组合功能 -l -p 8080 -d /usr/logs 异常情况 布尔类型:输入 -l a; -l 3 单整数类型:输入 -p a; -p 3.14 单个连续字符串类型:输入 -d a b c; -d /usr/logs /usr/logs/a.log 感悟:站在使用者的角度看待自己的代码

                作者回复: 纸上得来终觉浅,绝知此事要躬行。

                2022-03-20
                2
                1
              收起评论
              大纲
              固定大纲
              TDD 的基本原则
              命令行参数解析
              API 构思与组件划分
              功能分解与任务列表
              红 / 绿循环
              小结
              思考题
              显示
              设置
              留言
              38
              收藏
              42
              沉浸
              阅读
              分享
              手机端
              快捷键
              回顶部