TDD是测试驱动开发(Test-Driven Development)的缩写,是一种敏捷开发方法,它强调在编写代码之前先编写测试用例,并且这些测试用例是自动化的。通过不断地编写测试用例、运行测试用例、编写代码、重新运行测试用例和重构代码等步骤,来实现高质量的代码。 测试驱动开发是一种软件开发实践,源于1999年Kent Beck《Extreme Programming Explained》一书中的测试先行这一概念。Kent Beck 在2003年再次提到 – TDD鼓励简单的设计并激发信心。经过后期的发展,TDD已经成长为一门独立的软件开发技术,其名气甚至盖过了极限编程。
在实践TDD的过程中,人们对TDD形成了三层理解:
- Task-Driven Development,任务驱动开发。
- Test-Driven Development,测试驱动开发。
- Test-Driven Design,测试驱动设计。
TDD 三层解读
Task-Driven Development
任务拆解是TDD的一个关键的步骤。在开始编写进入代码环节之前,需要对业务需求做拆分,这个过程我们把它称之为Tasking。比如,一个用户登录的业务需求,我们大体可以拆分成如下几个Task:
- 假定用户名不存在时,当用户登录,则登录失败
- 假定用户名正确且密码错误时,当用户登录,则登录失败
- 假定用户名和密码都正确时,当用户登录,登录成功
通过上面的三个Tasking,可以总结出一个模式:Given When Then:
- Given 用户名不存在,When 用户登录, Then 登录失败
- Given 用户名正确且密码错误,When 用户登录, Then 登录失败
- Given 用户名和密码都正确,When 用户登录, Then 登录成功
Given-When-Then 借鉴了BDD(Behavior Driven Development)里提倡的模式,它更加关注用户如何使用系统,即系统所提供的功能,从理解上更偏向于业务语言。对于一些技术人员,在初学阶段,需要多去练习,体会不同点,可以选业务人员视角索要一些反馈。
在TDD中,我将之称为Tasking三步曲:
- Given,代表特定的业务场景
- When,代表用户发生的行为
- Then,代表行为产生的结果
Test-Driven Development
上一步Tasking拆分好的一系列Task,经过沟通澄清之后,就可以将这些Task翻译成测试。在翻译的过程中需要注意的一个核心点是 – 保持业务概念的统一(统一语言的应用)。如何理解这一点呢,来个看一个示例。 需求 : 我们定义了一个addOneYear函数,接受一个Date类型的参数date,返回一个新的Date类型的日期。该函数会将传入的日期增加一年,并返回新的日期。我们使用setFullYear方法来增加年份,然后返回新的日期
Task:
以下是对于"addOneYear"函数的Given-When-Then拆分:
-
Given 一个日期,When 调用addOneYear函数,Then 返回增加一年后的日期
翻译成测试代码后:
describe('addOneYear', () => {
it('should add one year to given date', () => {
const date = new Date('2022-01-01T00:00:00Z');
const newDate = addOneYear(date);
expect(newDate.getFullYear()).toEqual(2023);
});
});
addOneYear 实现
const addOneYear = (date: Date): Date => {
const newDate = new Date(date);
newDate.setFullYear(newDate.getFullYear() + 1);
return newDate;
};
在上述测试代码中,仅测试了纯函数,并没有体现具体业务概念。
Test-Driven Design
TDD并不会驱动出好的设计,TDD只会给你及时的反馈什么可能是糟糕的设计 – Kent Beck
TDD 执行流程
TDD采用了一种以终为始的思维方式,它依赖于非常短的“测试-实现-重构”的重复:先将需求转换为具体测试用例,然后编写代码让测试通过,然后按需做必要的重构,来改进代码。
实际编码过程体现为:在开发业务功能代码之前,先编写测试代码。测试代码确定了我们要验收什么以及如何验收,然后再去编写功能代码,当测试通过时,代表功能完成。
这个循环对应的具体操作流程图:
你可能会问:“我才新写了一个测试用例,明知道它会运行失败(另外还有编译错误),还要运行吗?”。
答案是要!为什么?
首先,作为新手,这是一种节奏感和习惯的养成,一些行为习惯会潜移默化地影响你思考方式。其次,测试失败有很多种情况,你需要通过运行测试来获得反馈,这个失败是不是你期望的失败,比如:
- 如期失败:是不是接口还不存在,编译错误?那你需要去创建各种类和方法
- 如期失败:是不是新功能逻辑还没有实现,断言失败?那你接下就需要去实现新功能
- 非如期失败:是不是不小心手误改了测试代码,导致其他的也失败了?那你需要当心了
- 非如期成功,测试直接通过了,我的测试是不是写的有问题?那我需要当心了