TDD和BDD的单测思维

TDD和BDD的单测思维

TDD和BDD是两种单测的指导思路,但是在实际生产中,我们实际上要不断地穿梭于这两种思路,才可以写出好的单元测试和好的业务代码。实际上它们的意义并不复杂,只是我们需要不断地思考和训练,才能把它们运用到不同的开发情况中。

TDD

TDD (Test-Driven Development) 也就是测试驱动开发。

测试驱动开发让我们着重于单元测试,先有单元测试后有接口,这会让我们想着如何让我们设计的接口通过单元测试,并且实现完整的代码覆盖。通过这样的方式,我们会写出一个个逻辑尽可能单一,if和for的嵌套更少的接口。

我们可以从一些例子看看,利用TDD的思维,会如何将我们的代码优化:

  • 假设我们有一份文档需要进行上传,但在上传之前,需要对其进行压缩、加密

优化前

代码大概会如下:

  1. if fileExists(path) {
  2. let file = FileHandle(path)
  3. let success = compress(file)
  4. if success {
  5. let success = encrypt(file)
  6. if success {
  7. let success = upload(file)
  8. if !success {
  9. // error handling…
  10. }
  11. } else {
  12. // error handling…
  13. }
  14. } else {
  15. // error handling…
  16. }
  17. } else {
  18. // error handling…
  19. }

这段代码演示了一段文件压缩、加密和上传的流程,但是要对这个接口进行单元测试,却并不是那么的简单和清晰。因为这个接口中,包含了太多的逻辑和太多的if嵌套,如果我们要对它进行一个完整的单元测试覆盖,这难度不亚于让一个普通人来一段即兴饶舌演奏。

优化后

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

  1. 统一的错误处理方法
    1. func errorHandling()
    2. {
    3. // error handling...
    4. }
  2. 对于上面的代码,其实我们可以看到比较统一的范式,就是先执行某事,如果它成功了,就执行下一件,如果失败了,就进行错误处理。这个范式,我们也可以封装成一个方法:
  1. func handle(first: ()->(Bool), then: (()->(Bool))?) -> Bool
  2. {
  3. if first() {
  4. return (then ?? { true })()
  5. } else {
  6. errorHandling()
  7. return false
  8. }
  9. }
  1. 原本的代码,我们就可以这样写了
  1. let res = handle {
  2. return fileExists(path)
  3. } then: {
  4. let file = FileHandle(path)
  5. handle(first: compress(file), then: handle(first: encrypt(file), then: upload(file)))
  6. }
  7. // handle res...

实际上,在代码经过上述的改动后,需要我们进行单测的点已经不多了,我们只需要专注于对错误处理、handle工具方法、以及独立的压缩、加密、上传等方法进行单元测试,基本就已经能保证最终成品方法的质量了。

这只是 TDD 中的冰山一角,如果我们从开发的初始阶段,就运用这种思维,我们会让单测的书写过程变成一片坦途。而如果我们对老代码以 TDD 的方式进行审视,我们就会发现很多可以重构的点。

BDD

BDD (Behavior-Driven Development) 也就是行为驱动开发。

BDD也是单元测试的一种指导性思维,和TDD相辅相成,如果说TDD旨在让我们写出更好的代码,来通过单元测试的话,而BDD更多地在于——消除因过度关注于接口的结果而产生的很多魔法数的描述,从而让我们写出更能长期使用,无需因需求或逻辑变更,而频繁改动的单元测试的代码。

TDD是只需要关注测试结果的,BDD让我们更关注代码的行为。

假定我们自定义实现了一个栈,然后我们需要对其进行单测。
我们大概会写出如下的单测用例:

  1. describe("Stack") {
  2. it("should have 1 element") {
  3. var stack = Stack()
  4. let val = 100
  5. stack.push(val)
  6. expect(stack.count).toEqual(1)
  7. }
  8. }

这种用例,毫无疑问可以在当下完成对该Stack的单元测试,但是它引入了魔法数1,而且在描述上,只关注结果,而不关注接口的表现。

实际上,一旦我们对该自定义的Stack进行一些修改——比如往该Stack中塞入初始占位对象,那上面的用例就无法通过了。

实际上,如果更贴近于Stack的功能性来编写单测的话,可以写出如下的单测用例:

  1. describe("Stack") {
  2. it("should push the element to top") {
  3. var stack = Stack()
  4. let val = 100
  5. stack.push(val)
  6. expect(stack.top).toEqual(val)
  7. }
  8. }

如果是要偏向于stack.count的接口语义来写的话,我们可以关注到push方法其实是让count增长了 1 的,而不是为了让count达到某个值。

  1. describe("Stack") {
  2. it("should increase stack count by 1") {
  3. var stack = Stack()
  4. let oldcnt = stack.count
  5. let val = 100
  6. stack.push(val)
  7. expect(stack.count).toEqual(oldcnt + 1)
  8. }
  9. }

总结

综上,TDD是为了解决接口难以测试,覆盖难以全面的问题;而BDD实际是为了使单测更便于长期维护。TDD和BDD不过是两种编写单测的指导思路,实际上要写好单测,还需要多思考和实践。


文章标签:

原文连接:https://juejin.cn/post/7007348742927941668

相关推荐

Taro框架完美使用Axios

看完这篇,你也可以搞定有趣的动态曲线绘制

Codable保姆级攻略

iOS CarPlay|使用 CarPlay 为你的 App 提速

零基础教你Unity集成IOS原生本地推送

StoreKit2 实际接入时候的踩坑与解决实录

iOS:runloop 运行循环

第四届青训营阅读打卡活动来啦,奖品、规则全面升级,快来学习吧

支持SwiftUI!Swift版图片&视频浏览器-JFHeroBrowser上线啦

用 JavaScript 复原何同学B站头图、对前端构建工具的一些理解、弹幕的常规设计与实现 丨酱酱的下午茶第31期

现今 Swift 包中的二进制目标

[Android开发学iOS系列] 语言篇: Swift vs Kotlin

LeetCode - #125 验证回文串

通过Vue自定义指令实现前端埋点,我不写单元测试,被批了,利用噪声构建美妙的 CS,Kotlin协程-CoroutineScope丨酱酱的下午茶第30期

iOS 数据存储

iOS怎么用代码实现这样奇怪的动画

利用 UIScrollView 实现六棱柱图片浏览效果

iOS中为什么会有这么多锁呢?

免费ios开发流程和步骤教程

[LD]iOS二进制组件化与Protocol的潜在风险