TDD和BDD的单测思维

1,152 阅读4分钟

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嵌套,如果我们要对它进行一个完整的单元测试覆盖,这难度不亚于让一个普通人来一段即兴饶舌演奏。

优化后

但是,如果我们从优先考虑单元测试的角度,上面的代码我们可能会这么写:

  1. 统一的错误处理方法
func errorHandling() 
{
    // error handling...
}
  1. 对于上面的代码,其实我们可以看到比较统一的范式,就是先执行某事,如果它成功了,就执行下一件,如果失败了,就进行错误处理。这个范式,我们也可以封装成一个方法:
func handle(first: ()->(Bool), then: (()->(Bool))?) -> Bool
{
    if first() {
        return (then ?? { true })()
    } else {
        errorHandling()
        return false
    }
}
  1. 原本的代码,我们就可以这样写了
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不过是两种编写单测的指导思路,实际上要写好单测,还需要多思考和实践。