当前位置: 代码迷 >> 驱动开发 >> 测试驱动开发(TDD)在海外组的实践小结
  详细解决方案

测试驱动开发(TDD)在海外组的实践小结

热度:501   发布时间:2016-04-28 09:57:52.0
测试驱动开发(TDD)在海外组的实践总结

测试驱动开发(TDD, Test Driven Development)是什么?

测试驱动开发是一套开发方法论, 有经验的开发人员都会对自己的代码编写测试, 而测试驱动试图将这一过程做到极致, “如果测试被证明是有价值的, 那么, 我们为什么不能更频繁的去做测试, 如果将测试时间提前有益于提高应用质量, 那么, 为什么不先做测试, 再编写应用.”

测试驱动开发要求在编写某个功能之前先编写测试代码, 然后编写使测试通过的代码, 通过测试来推动整个开发的进行.

测试驱动开发最早由Kent Beck提出, 他还出过一本书, 叫测试驱动开发, 感兴趣的同学, 可以买(download)来看看.

本文主要介绍海外的一系列技术实践, 不打算做TDD的科普, 感兴趣的同学请自行google. 另外, 既然是技术实践, 就不会纠结在方法论上, 所以, 没有做过TDD, 没有写过单元测试的同学无需过分担忧, 你还是能看懂的.

实践一: TDD的核心是任务拆分.

咦, 不是写测试, 是任务拆分. 是的, 跟我合作过的同学, 尤其是新人同学大概都有印象, 我一般会要求(我自己也常常这么干)把要做的事情写在一张卡片上, 写成多行, 每行代表一件事. 每完成一件, 划掉一行, 或者在前面打钩.

  1. 为什么要写出来?
    帮助你把事情想清楚, 每个人也许都曾感到, 想得东西很多, 结果却不知从何处下笔, 因为开始写了, 你才会真正得考虑上下文管理得严谨性, 才会推敲每一个字的对错.
    完成一件事, 花掉一行会给人以making progress的正向反馈, 你会觉得有成就感. 不信你试试. 当你一堆的卡片都是打钩状态时, 你会觉得自己效率很高, 从而自信满满. 工作效率提升也是有可能的.

  2. 一定要写出来么?
    是的, 没有比这更好的办法, 用手机, 绝对不超过十分钟, 你就会被微信,QQ,邮箱,各种推送打扰. 用大脑, 当你对某个事情非常有经验的时候, 可以, 但是相信我, 让你的宝贵脑细胞既负责思考问题的解决方案(这是有价值的), 又负责记忆下一步要做什么, 是一个很奢侈的浪费. 把这些琐碎而又low level的的记忆工作交给纸片吧, 写下来, 你只需要定时回顾, 确保自己在正确的事情上花费了合理的时间, 就够了.

  3. 怎么写?
    这其实是今天的重点, 当你决定开始任务拆分了, 并且已经准备了纸和笔, let’s begin.

A. 从业务出发.
从业务出发强调了, 你应该首先关注的是要解决的问题, 而不是工具,语言, 架构设计和框架. 就我的经验, 很多人, 特别是初入职场的同学, 最先暴露出来的问题就是不能清楚得定义问题.
比如, 我们要做一个病历同步的功能, 那么, 上传的时候要不要考虑http请求因为网络连接问题导致失败后的重试, 于是我们花大量时间研究怎么优雅得实现retry. 但是, 回过头来再看一下需求, 我们要对病历同步, 所有的病历数据是本地存储的, 同步检查每两分钟就会自动触发一次, 换句话说, 我们不需要考虑retry, 因为, 没有成功上传的病历, 再两分钟后会再次被同步. 因此, 我们要解决的问题变成了, 确保上传失败的病历, 其同步状态不会被改变. (该案例供参考, 不代表现在的同步逻辑)
此刻的任务列表

  • 上传病历
  • 上传失败的病历不更新同步状态

B, 面向架构细分任务.
经过上面的思考, 我们得到了一个额外的任务, 然而, 上述两个任务都是无法进行任何编码工作的, 它们还是太抽象了, 下一步该如何拆分, 这时, 我们的架构终于起到了作用, 对于一个应用, 我们一般都会有一个架构模式, 如MVC&MVP, 如Redux/Reflux, 如Rx, 再比如网络层或者中间件, 根据以上, 我们可以编写进一步得拆分了, 如果是MVP, 那么应该有对Model层的数据读写, Presenter对UI的更新及Viewer的响应, 此刻, 任务列表应该有所更新了.

  • 上传病历
  • Model(更可以细分成读和写)
  • Network(Url, params, payload, response)
  • Viewer.showLoading/success/failure
  • 上传失败的病历不更新同步状态
  • 定时器?

C, 考虑边界条件, 像QA一样思考
- 网络失败
- 用户数据不合法(对于同步, 这其实是一个不合理的任务, 数据验证应该在界面提交时就完成了, 所以删除它)

D, 不断细化你的任务列表,
坚持. 当你看到一个需求就像庖丁看到牛, 也许, 你可以考虑不写任务列表了.

任务列表是测试驱动开发的核心, 继续谈一下测试实现过程中的考量.

1, 测试即文档, 可读性优先.

1)测试代码是像生产代码一样, 需要持续维护的, 因此, 它必须写好. 由于测试代码并不会在高并发的环境下运行, 因此不用过早地在意性能的问题, 而是优先考虑测试代码的可读性.

在早期的基于JUnit的单元测试中, 技术人员通常喜欢在方法名上做文章. 如以下几种形式都广为使用.

public void test_email_can_not_duplicate() {...}
@Testpublic void should_throw_error_when_email_has_already_been_used() {...}

后来, 社区中有人提出了BDD的概念, 而后, 测试框架也在逐步演进, 有了cucumber和JBehave, 于是有了下面的写法

@Given('I have 10 dollars')public void i_have_10_dollars() {...}

将对测试的描述以更为自然语言的方式表达, 当你的测试量逐渐增加, 到几千个的时候, 有种写小说的感脚. 然而太啰嗦, 我不太喜欢, 我们在海外组使用的是mocha, 支持上述几种风格的描述.

describe('POST /patients', () => {  it('should respond 200 after creating patients', (done) => {...})  it('should respond 401 when create time is not set', (done) => {...})  ...})

写完就是一篇华丽丽的接口文档, 后来我们还引入了supersamples, 感兴趣的同学到这里看demo, 碉堡了, 有没有. 类似的工具很多, 以前还用过http://concordion.org/, 也是无比华丽.

2) 测试三段论Given/When/Then
测试也是代码, 既然是代码, 就需要良好的设计, Given/When/Then被称为测试三段论, 是一种常见的用于测试的模式, 很多同学做了任务拆分, 但是测试还是写不好, 往往是三段论没考虑清楚.

Given, 前提条件, When, 测试点或者说被测方法, Then, 结果验证. 比如, 我们要测试删除功能. 那么, 前提就是你要有一条数据可删.

// Givendb('accounts').insert({id: 'xxx', lastName: 'xxxx'})// Whenconst result = await Account.deleteById(id)// ThenassertTrue(result)

当你不知道从何开始写测试的时候, 首先搞清楚你要测什么, 它的前提是什么, 如何验证被测试的函数执行成功了.

多说一句, 经常写单元测试的人, 尤其是通过TDD的方式, 写出来的代码可读性和可维护性都不会差, 单元测试鼓励你写出纯函数, 写出简洁的代码.

3) 测试方法必须是幂等的, 即可重复执行的, 测试之间不能有依赖.

说实话, 这其实很难, 很多人半途而废, 觉得测试越写越复杂, 甚至互相影响, 一个挂了, 红掉一片, 就是这条没坚持好.

比如, 有一些集成测试, 会往数据库里写数据, 就像上面的例子, 那么, 很重要的一点就是, 测试完成要还原数据库.

感谢极限编程社区, 现在做这些事已经不那么麻烦. 哦, 应该说很容易. 以海外为例.

我们使用knex来简化对数据库的操作, 与之相配合的, 有一个叫knex-cleaner的工具, 专门负责对数据库的操作进行重置, 感兴趣的同学可以来海外观摩或者参考文档.

借助mocha的beforeEach和afterEach hook, 有了下面的代码, 很简单吧, 测试代码就不需要关注数据库环境了, 当做全新的即可.

beforeEach(() => knexCleaner.clean(db))afterEach(() => knexCleaner.clean(db))

但是, 有同学可能会问, 难道我做测试每次都要准备数据么, 那多麻烦. 答案如下

  1. 如果你每次要准备多复杂的数据, 考虑你的测试/程序/数据库设计是否合理, 复杂的数据已经暗示你的测试可能关注的不是一件事, 那么应该果断考虑拆分.
  2. 如果确认不需要拆分, 那么没别的办法, 写一个数据的factory吧, 用于生成测试数据, 这并不麻烦, 另外, factory还能复用, 你肯定不会只用一次.
  3. 还有一种情况, 就是代码会依赖于第三方服务的时候, 比如海外的代码就会依赖aws的api, 那怎么办?
    答案, 把第三方的api mock或者stub掉, 这里就涉及另外的技术和工具了, 篇幅所限, 本篇暂不展开

4) 测试是演进的, 需要持续重构.

某种程度上说, 测试代码对代码质量的要求是高于生产代码的.

5) 代码评审(code review)时, 先review测试.

好了, 关于测试代码的注意事项, 先写到这里. 单元测试会导致你的代码量double, 这个成本值得么, 我来告诉你, 非常值得.

短期内, 它强迫你关注在业务/架构/问题本身, 而不是想入非非的”可扩展的设计”, 即更聚焦.

长期来看, 它会让你对代码重构更有信心, 你可以更加放心的优化你的代码, 替换第三方库到新版本, 甚至某种程度上做底层架构改变(比如升级babel6).

                                                        杏树林研发  秦汉
  相关解决方案