Web 前端单元测试到底要怎么写?看这一篇就够了

随着 Web 应用的复杂程度越来越高,很多公司越来越重视前端单元测试。我们看到的大多数教程都会讲单元测试的重要性、一些有代表性的测试框架 api 怎么使用,但在实际项目中单元测试要怎么下手?测试用例应该包含哪些具体内容呢?

本文从一个真实的应用场景出发,从设计模式、代码结构来分析单元测试应该包含哪些内容,具体测试用例怎么写,希望看到的童鞋都能有所收获。

项目用到的技术框架

该项目采用 react 技术栈,用到的主要框架包括:reactreduxreact-reduxredux-actionsreselectredux-sagaseamless-immutableantd

应用场景介绍

图片描述

这个应用场景从 UI 层来讲主要由两个部分组成:

  • 工具栏,包含刷新按钮、关键字搜索框
  • 表格展示,采用分页的形式浏览

看到这里有的童鞋可能会说:切!这么简单的界面和业务逻辑,还是真实场景吗,还需要写神马单元测试吗?

别急,为了保证文章的阅读体验和长度适中,能讲清楚问题的简洁场景就是好场景不是吗?慢慢往下看。

设计模式与结构分析

在这个场景设计开发中,我们严格遵守 redux 单向数据流 与 react-redux 的最佳实践,并采用 redux-saga 来处理业务流,reselect 来处理状态缓存,通过 fetch 来调用后台接口,与真实的项目没有差异。

分层设计与代码组织如下所示:
图片描述

中间 store 中的内容都是 redux 相关的,看名称应该都能知道意思了。

具体的代码请看 这里

单元测试部分介绍

先讲一下用到了哪些测试框架和工具,主要内容包括:

  • jest ,测试框架
  • enzyme ,专测 react ui 层
  • sinon ,具有独立的 fakes、spies、stubs、mocks 功能库
  • nock ,模拟 HTTP Server

如果有童鞋对上面这些使用和配置不熟的话,直接看官方文档吧,比任何教程都写的好。

接下来,我们就开始编写具体的测试用例代码了,下面会针对每个层面给出代码片段和解析。那么我们先从 actions 开始吧。

为使文章尽量简短、清晰,下面的代码片段不是每个文件的完整内容,完整内容在 这里

actions

业务里面我使用了 redux-actions 来产生 action,这里用工具栏做示例,先看一段业务代码:

对于 actions 测试,我们主要是验证产生的 action 对象是否正确:

这个测试用例的逻辑很简单,首先构建一个我们期望的结果,然后调用业务代码,最后验证业务代码的运行结果与期望是否一致。这就是写测试用例的基本套路。

我们在写测试用例时尽量保持用例的单一职责,不要覆盖太多不同的业务范围。测试用例数量可以有很多个,但每个都不应该很复杂。

reducers

接着是 reducers,依然采用 redux-actionshandleActions 来编写 reducer,这里用表格的来做示例:

这里的状态对象使用了 seamless-immutable

对于 reducer,我们主要测试两个方面:

  1. 对于未知的 action.type ,是否能返回当前状态。
  2. 对于每个业务 type ,是否都返回了经过正确处理的状态。

下面是针对以上两点的测试代码:

这里的测试用例逻辑也很简单,依然是上面断言期望结果的套路。下面是 selectors 的部分。

selectors

selector 的作用是获取对应业务的状态,这里使用了 reselect 来做缓存,防止 state 未改变的情况下重新计算,先看一下表格的 selector 代码:

这里的分页器部分参数在项目中是统一设置,所以 reselect 很好的完成了这个工作:如果业务状态不变,直接返回上次的缓存。分页器默认设置如下:

那么我们的测试也主要是两个方面:

  1. 对于业务 selector ,是否返回了正确的内容。
  2. 缓存功能是否正常。

测试代码如下:

测试用例依然很简单有木有?保持这个节奏就对了。下面来讲下稍微有点复杂的地方,sagas 部分。

sagas

这里我用了 redux-saga 处理业务流,这里具体也就是异步调用 api 请求数据,处理成功结果和错误结果等。

可能有的童鞋觉得搞这么复杂干嘛,异步请求用个 redux-thunk 不就完事了吗?别急,耐心看完你就明白了。

这里有必要大概介绍下 redux-saga 的工作方式。saga 是一种 es6 的生成器函数 – Generator ,我们利用他来产生各种声明式的 effects ,由 redux-saga 引擎来消化处理,推动业务进行。

这里我们来看看获取表格数据的业务代码:

不熟悉 redux-saga 的童鞋也不要太在意代码的具体写法,看注释应该能了解这个业务的具体步骤:

  1. 从对应的 state 里取到调用 api 时需要的参数部分(搜索关键字、分页),这里调用了刚才的 selector。
  2. 组合好参数并调用对应的 api 层。
  3. 如果正常返回结果,则发送成功 action 通知 reducer 更新状态。
  4. 如果错误返回,则发送错误 action 通知 reducer。

那么具体的测试用例应该怎么写呢?我们都知道这种业务代码涉及到了 api 或其他层的调用,如果要写单元测试必须做一些 mock 之类来防止真正调用 api 层,下面我们来看一下 怎么针对这个 saga 来写测试用例:

这个测试用例相比前面的复杂了一些,我们先来说下测试 saga 的原理。前面说过 saga 实际上是返回各种声明式的 effects ,然后由引擎来真正执行。所以我们测试的目的就是要看 effects 的产生是否符合预期。那么effect 到底是个神马东西呢?其实就是字面量对象!

我们可以用在业务代码同样的方式来产生这些字面量对象,对于字面量对象的断言就非常简单了,并且没有直接调用 api 层,就用不着做 mock 咯!这个测试用例的步骤就是利用生成器函数一步步的产生下一个 effect ,然后断言比较。

从上面的注释 3、4 可以看到,redux-saga 还提供了一些辅助函数来方便的处理分支断点。

这也是我选择 redux-saga 的原因:强大并且利于测试。

api 和 fetch 工具库

接下来就是api 层相关的了。前面讲过调用后台请求是用的 fetch ,我封装了两个方法来简化调用和结果处理:getJSON()postJSON() ,分别对应 GET 、POST 请求。先来看看 api 层代码:

业务代码很简单,那么测试用例也很简单:

由于 api 层直接调用了工具库,所以这里用 sinon.stub() 来替换工具库达到测试目的。

接着就是测试自己封装的 fetch 工具库了,这里 fetch 我是用的 isomorphic-fetch ,所以选择了 nock 来模拟 Server 进行测试,主要是测试正常访问返回结果和模拟服务器异常等,示例片段如下:

基本也没什么复杂的,主要注意 fetch 是 promise 返回,jest 的各种异步测试方案都能很好满足。

剩下的部分就是跟 UI 相关的了。

容器组件

容器组件的主要目的是传递 state 和 actions,看下工具栏的容器组件代码:

那么测试用例的目的也是检查这些,这里使用了 redux-mock-store 来模拟 redux 的 store :

很简单有木有,所以也没啥可说的了。

UI 组件

这里以表格组件作为示例,我们将直接来看测试用例是怎么写。一般来说 UI 组件我们主要测试以下几个方面:

  • 是否渲染了正确的 DOM 结构
  • 样式是否正确
  • 业务逻辑触发是否正确

下面是测试用例代码:

得益于设计分层的合理性,我们很容易利用构造 props 来达到测试目的,结合 enzymesinon ,测试用例依然保持简单的节奏。

总结

以上就是这个场景完整的测试用例编写思路和示例代码,文中提及的思路方法也完全可以用在 VueAngular 项目上。完整的代码内容在 这里 (重要的事情多说几遍,各位童鞋觉得好帮忙去给个 哈)。

最后我们可以利用覆盖率来看下用例的覆盖程度是否足够(一般来说不用刻意追求 100%,根据实际情况来定):
图片描述

单元测试是 TDD 测试驱动开发的基础。从以上整个过程可以看出,好的设计分层是很容易编写测试用例的,单元测试不单单只是为了保证代码质量:他会逼着你思考代码设计的合理性,拒绝面条代码

借用 Clean Code 的结束语:

2005 年,在参加于丹佛举行的敏捷大会时,Elisabeth Hedrickson 递给我一条类似 Lance Armstrong 热销的那种绿色腕带。这条腕带上面写着“沉迷测试”(Test Obsessed)的字样。我高兴地戴上,并自豪地一直系着。自从 1999 年从 Kent Beck 那儿学到 TDD 以来,我的确迷上了测试驱动开发。

不过跟着就发生了些奇事。我发现自己无法取下腕带。不仅是因为腕带很紧,而且那也是条精神上的紧箍咒。那腕带就是我职业道德的宣告,也是我承诺尽己所能写出最好代码的提示。取下它,仿佛就是违背了这些宣告和承诺似的。

所以它还在我的手腕上。在写代码时,我用余光瞟见它。它一直提醒我,我做了写出整洁代码的承诺。

1 1 收藏 评论

可能感兴趣的话题



直接登录
跳到底部
返回顶部