最近在组里我又领了一个新任务:前端单元测试。
关于这个话题在很早的时候就想和大家聊了,奈何一直没机会。对于我个人来说,我是非常喜欢写单测的。最近还买了本《软件测试》的书,算是再次复习一下大学时学过的专业课,平时在捣鼓一些个人项目的时候也会做一些基础的单测。
一谈到单测,可能大家的第一反应都是敬而远之。
没啥用,没时间,我不会
我承认写单测是个非常有挑战性,且难度不小的活,但 我依然推荐大家尝试去写一写单元测试,因为它所带来的好处不仅仅是大家想的那么简单:“只是 Bug 少了一点”。所以,今天我会尝试从另外一些角度来讨论单测可以给我们带来哪些好处。
接着刚刚说到的 “只是 Bug 少一点” 这句话,可能大多数觉得单测就是在提测前减少一点 Bug 而已:
这样的想法确实是最直观的。但这只是想到了第一层,如果我们把 开发流程所有步骤 都加进来,会发现是这样的:
在 开发过程
后面,几乎每个流程都可能抛出 Bug。越是到后面流程才抛出的 Bug,程序员就越是要投入比开发阶段更大的时间和业务,而且所承受的风险也是最高的。
或许大家会想:不就改个 Bug,改几行而已。可是大家有没有想过在跟测的过程中,很可能你已经开始另一个需求的评审了! 此时的你在解决突然插入的 Bug 的时候,心态还会像刚开始写代码时候那么轻松么?
实际上,还有更多的隐性成本没有考虑,比如反复确认产品逻辑、反复确认交互设计、反复确认前后端接口设计、各端对产品的理解。 有的时候,你就会发现这样很魔幻的场景:明明是一个字段的展示问题,竟然要花上一上午,拉了 4、5 个人来开会核对的情况。
下面这张图,也在说明两个问题:一是 85% 的缺陷都在代码设计阶段产生;二是发现 Bug 的阶段越靠后,耗费成本就越高,呈指数级别的增长。这种 “指数成本” 的案例也经常发生,当我们改正一个 Bug 的时候,可能随之而来又会多出 3 个 Bug,俗称:改崩了。
所以,在早期的单元测试就能发现bug,不仅可以省时省力,在开发流程上提高效率,也能降低反复修改出现的风险和时间成本。
这一节主题就是大家经常想的:减少 Bug 率。我们不妨来想一个问题:什么才是 Bug? 相信所有开发人员都不愿意写 Bug,在 《软件测试》 这本书中将 Bug 描述成 “软件缺陷”,里面说道:
大多数的 “软件缺陷” 并非源自编程错误,对众多从小到大的项目进行研究而得出的结论往往是一致的,导致软件缺陷最大的原因是产品说明书!见下图
大多数的产品还是能够写出一份清晰明了的需求单的,奈何 ta 也不可能把所有情况想都枚举出来,这也导致了开发时很容易出现考虑不周全的情况。往往能够发现异常情况的人要么是测试、要么是交互视觉、要么是后期产品体验。 那到这个时候才发现的问题,然后再去修复又会出现的 指数爆炸的成本。
如果把实现功能看成走迷宫,把找到通路看成上线需求, 那么编码实现的过程就像从入口找出口,而单元测试则像从出口找入口。 这种开两个线程 “双向奔赴” 的找通路方法能够用最精准最快的方式找到通路。
单测所保障的不仅仅只是代码的正确性,毕竟大家在边开发边 Debug 的时候已经能验证 99% 的正确性了,而单测更大的地方在于 让我们不得不去思考到一些异常情况 ,这无形中就能增强代码的质量。
可能大家对上面这一节也不以为意,我能理解大家的侥幸心理。毕竟在公司里,开发写完 Bug,然后交给测试找出来是大家其乐融融表现。而且不写测试大家过得还挺好的,也没出什么大乱子。
造成这样的错觉在两个方面:一是测试找 Bug,开发再 Debug,这确实能解决燃眉之急,短期内很有效果。二是需求一直不断快速迭代,一期的 Bug,二期还能合着去改,二期改不了还有三期,三期结束了还有四期...... 。
这种永无止境的测试 + 开发模式能在一定程度上让我们的代码 “看起来是有保障的” 。
人肉测试固然好用,但是也有下面的缺点:
由于成本很高,人肉测试一般只会用来测业务功能,并没有太多测试资源可以分配到优化需求、技术需求上。所以对于这类需求只能通过前端开发人员自测,到目前为止也只是优化一个点,然后点点鼠标来自测,效率并不高。一旦优化过程中改出了问题,回滚、和修复的成本又会非常高,这也会助长大家 “不敢优化”、“能不动就不动” 的思想。
如果能有一定量的测试,则有足够强大的信心来支撑项目的优化,也有助于整个项目的未来发展和改进。
测试驱动开发(Testing-Driven Development)是敏捷开发中的一项核心实践和技术,也是一种设计方法论。
上面说的单测特点比较偏向于 “防守”,而 TDD 中的测试则偏向于 “进攻”。 TDD 的原理是在开发功能代码之前,先编写单元测试用例代码,在此基础上再补充产品代码。比如要实现 getUserById
这个服务,那么可以先写如下测试,然后再补充 getUserById
的实现:
describe('getUserById', () => {
it('可以根据 id 返回用户信息', () => {
// TODO: getUserById 未实现
const user = getUserById('122');
expect(user.id).toEqual('122');
})
})
这种方法在 Node 端非常实用。由于 Node 端要依赖的项非常多,比如数据库、各方接口、配置中心等等。每次用 Postman 去测接口,就会一次性将多个模块以及服务一起测了。如果别的服务还在开发或者有问题,就会直接阻塞了接口的开发。
虽然 Postman 在接口测试的时候很好用,但是它也有如下缺点:
单元测试则很好地填补了这一块,利用单测强大的 Mock 能力先将依赖项都 Mock 掉,开发时可以只关注某个函数、服务的开发,不会受其依赖项干扰:
由于每次提交代码都应该保证测试通过率 100%,所以我们也不会担心这些例子是否过期的问题。
测试用例还有个很好的功能:将使用案例记录在案。
很多时候别人写一些工具函数和方法,使用者是不能一眼就能学会怎么用的。往往这时写函数的人就会说:你看 XXX 文件就知道怎么用了。 但这些 “真实例子” 中通常会夹杂着很多依赖项,无法作用一个最小 Use Case 来理解。
而单测里的每个用例都可以看成一个最小的 example
,通过阅读 Test Case 就能马上知道这个函数怎么使用了。 这里举 redux 的 compose
函数的例子:
describe('Utils', () => {
describe('compose', () => {
it('composes from right to left', () => {
const double = (x: number) => x * 2
const square = (x: number) => x * x
expect(compose(square)(5)).toBe(25)
expect(compose(square, double)(5)).toBe(100)
expect(compose(double, square, double)(5)).toBe(200)
})
})
}
就算我们不知道 compose
是用来干嘛的,但是我们很清楚地知道,使用方法就是从右到左地执行回调。
由于每次发布时我们都要保证单测 100% 通过率,所以永远不用担心这个 Use Case 无法使用、过期的问题。
抛开这些项目质量、优化流程的原因,推荐大家写单测的另一重要原因就是 提升个人能力。
几乎所有 Jest 的入门文章的开头都会有一个非常简单的 Test Case:
expect(1 + 1).toEqual(2)
这很容易让人误以为单测很简单,以为不就是学一个框架那样嘛。然而,只有在真正编写测试用例的时候才会发现单测的难度呈指数级上涨。因为测试的本身是另一个领域,是需要通过不断练习才能掌握测试技巧的。 对前端单测来说,它的难度包括但不限于如下几点:
localStorage
,cookie
这些浏览器独有的东西时,Jest 就会报错,很多人受不了直接放弃了@nestjs/testing
,React.js 有 react-testing-library
。有的库 Redux 又会有自己独特的 testing guide总的来说,写单测并不像大家想的这么简单,jest
只是个开始的地方。不过,从另一个角度来看,如果你能坚持写好单测,对个人能力也大有裨益:
稍微总结一下,单测可以在 优化开发流程、保证项目质量、给项目优化上保险、驱动开发、提供 Use Case、提升个人能力 方面有着非常大的益处。
当然,本文也并非要让大家马上给项目上单测,只是希望大家能够多尝试自己领域之外的东西,不要固步自封。对个人而言,多练习写单测能力肯定是好处多于坏处。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8