TDD和BDD的单测思维
TDD和BDD是两种单测的指导思路,但是在实际生产中,我们实际上要不断地穿梭于这两种思路,才可以写出好的单元测试和好的业务代码。实际上它们的意义并不复杂,只是我们需要不断地思考和训练,才能把它们运用到不同的开发情况中。
TDD
TDD (Test-Driven Development) 也就是测试驱动开发。
测试驱动开发让我们着重于单元测试,先有单元测试后有接口,这会让我们想着如何让我们设计的接口通过单元测试,并且实现完整的代码覆盖。通过这样的方式,我们会写出一个个逻辑尽可能单一,if和for的嵌套更少的接口。
我们可以从一些例子看看,利用TDD的思维,会如何将我们的代码优化:
- 假设我们有一份文档需要进行上传,但在上传之前,需要对其进行压缩、加密
优化前
代码大概会如下:
if fileExists(path) {
let file = FileHandle(path)
let success = compress(file)
if success {
let success = encrypt(file)
if success {
let success = upload(file)
if !success {
// error handling…
}
} else {
// error handling…
}
} else {
// error handling…
}
} else {
// error handling…
}
复制代码
这段代码演示了一段文件压缩、加密和上传的流程,但是要对这个接口进行单元测试,却并不是那么的简单和清晰。因为这个接口中,包含了太多的逻辑和太多的if
嵌套,如果我们要对它进行一个完整的单元测试覆盖,这难度不亚于让一个普通人来一段即兴饶舌演奏。
优化后
但是,如果我们从优先考虑单元测试的角度,上面的代码我们可能会这么写:
- 统一的错误处理方法
func errorHandling()
{
// error handling...
}
复制代码
- 对于上面的代码,其实我们可以看到比较统一的范式,就是先执行某事,如果它成功了,就执行下一件,如果失败了,就进行错误处理。这个范式,我们也可以封装成一个方法:
func handle(first: ()->(Bool), then: (()->(Bool))?) -> Bool
{
if first() {
return (then ?? { true })()
} else {
errorHandling()
return false
}
}
复制代码
- 原本的代码,我们就可以这样写了
let res = handle {
return fileExists(path)
} then: {
let file = FileHandle(path)
handle(first: compress(file), then: handle(first: encrypt(file), then: upload(file)))
}
// handle res...
复制代码
实际上,在代码经过上述的改动后,需要我们进行单测的点已经不多了,我们只需要专注于对错误处理、handle
工具方法、以及独立的压缩、加密、上传等方法进行单元测试,基本就已经能保证最终成品方法的质量了。
这只是 TDD 中的冰山一角,如果我们从开发的初始阶段,就运用这种思维,我们会让单测的书写过程变成一片坦途。而如果我们对老代码以 TDD 的方式进行审视,我们就会发现很多可以重构的点。
BDD
BDD (Behavior-Driven Development) 也就是行为驱动开发。
BDD也是单元测试的一种指导性思维,和TDD相辅相成,如果说TDD旨在让我们写出更好的代码,来通过单元测试的话,而BDD更多地在于——消除因过度关注于接口的结果而产生的很多魔法数的描述,从而让我们写出更能长期使用,无需因需求或逻辑变更,而频繁改动的单元测试的代码。
TDD是只需要关注测试结果的,BDD让我们更关注代码的行为。
假定我们自定义实现了一个栈,然后我们需要对其进行单测。
我们大概会写出如下的单测用例:
describe("Stack") {
it("should have 1 element") {
var stack = Stack()
let val = 100
stack.push(val)
expect(stack.count).toEqual(1)
}
}
复制代码
这种用例,毫无疑问可以在当下完成对该Stack
的单元测试,但是它引入了魔法数1,而且在描述上,只关注结果,而不关注接口的表现。
实际上,一旦我们对该自定义的Stack
进行一些修改——比如往该Stack
中塞入初始占位对象,那上面的用例就无法通过了。
实际上,如果更贴近于Stack
的功能性来编写单测的话,可以写出如下的单测用例:
describe("Stack") {
it("should push the element to top") {
var stack = Stack()
let val = 100
stack.push(val)
expect(stack.top).toEqual(val)
}
}
复制代码
如果是要偏向于stack.count
的接口语义来写的话,我们可以关注到push
方法其实是让count
增长了 1 的,而不是为了让count
达到某个值。
describe("Stack") {
it("should increase stack count by 1") {
var stack = Stack()
let oldcnt = stack.count
let val = 100
stack.push(val)
expect(stack.count).toEqual(oldcnt + 1)
}
}
复制代码
总结
综上,TDD是为了解决接口难以测试,覆盖难以全面的问题;而BDD实际是为了使单测更便于长期维护。TDD和BDD不过是两种编写单测的指导思路,实际上要写好单测,还需要多思考和实践。