玩转UnitTest之巧妙处理依赖
0x00 引言
大家好,我是以卖码为生的海门。今天想和大家探讨一下如何巧妙处理依赖,玩转 UnitTest。
我们平时使用的网络请求库 Axios 等,这些对我们的项目来说都是依赖,可以说依赖无处不在,它的存在更容易发起网络请求,格式化时间等,但同样也带来了问题 - 很难为代码编写单元测试。
0x01 让我们开始吧
在正式进入主题之前,我会先告诉你,为了让我们的单测达到 F.A.I.R 原则的标准,我们需要解决的问题是:
- 功能代码使用良好的设计,对依赖进行解耦,尽可能去除依赖;
- 实在去除不了的依赖,如内部依赖,通过依赖注入构建松耦合的代码。测试代码使用一些技术进行替代,我们称这些技术叫测试替身 - 包括 Dummy、Fake、Stub、Mock和Spy。
需求:通过getCurrentPosition函数获取经纬度,定义一个供百度地图使用的 URL ,将该 URL 赋值给 window.location 。如果获取位置失败,则设置一条错误消息。
通过简单的需求分析和任务拆分,都会认为这个需求很简单。可是我们如何为它写自动化测试呢?没有写过UT的我,刚开始也是很懵的。跟我刚开始被要求为功能写单元测试的时候一样懵,完全不知道从那入手~我在搜索引擎中找到了一个解决方法,即借助。
1、借助Spike技术编写原型代码
2、从中获得一些信息
- getCurrentPosition是一个异步函数,包含成功和失败两个回调函数
- 获取到经纬度时,拼接成一条URL
- 未获取到经纬度时,设置错误信息
- 将 url 赋值给 location,此时浏览器会发生重定向
通过对原型代码的观察,发现其可测试性很差,而我们知道代码是否具有可测试性是个设计问题。所以,我们将解决第一个问题,即尽可能去除被测代码中的依赖。抽取函数,并将最少的数据作为参数传给它,对代码进行模块化设计。
3、模块化设计
有没有发现自己很多时候写的代码其实是原型代码,并没有设计可言!很多时候不写测试,根本发现不了这些问题,也不会对代码的设计有任何想法,更不用说什么设计模式地使用了。
接下来我们将 spike 阶段的原型代码进行拆分,是每个小功能都是一个小函数,每个小函数具有单一职责和最少依赖的特征。
下边需要做的就是为每一个函数编写自动化测试,以验证它们的行为。 依赖在整个问题中占据了很重要的作用,编写自动化测试的第一步就是确定一个或多个不具有内部依赖的函数,这些函数应该成为自动化测试的起点。
createUrl 就是我们要找函数,因为它没有任何其他依赖的代码测起来相对容易,只需要接收到正确的位置信息,然后创建一条符合预期的URL 就可以了。
接下来根据测试列表中的用例为其编写同步测试、反向测试、异常测试,之前介绍过方法就不再赘述了。
4、使用测试替身
接下来就是有依赖需要处理的函数,我们需要使用替身来解决第二个问题。
什么是替身? 代替真正依赖的对象,从而让自动化测试可以进行。就好比电影中的替身演员,当主角完不成不了某专业且高难度的动作时,此时替身演员就会上场,而替身演员的目的是为了让电影继续拍下去,比如主角没有找替身,要是残了的话,这部电影岂不是拍不下去了?! 如果还不了解这五种类型的测试替身,可以先花点时间看看。
我们将为依赖 window 对象的 location 属性的 setLocation 函数编写测试。首先试想一下,我们在没有单元测试的时候,是不是这样调试的?在这个方法打一个断点,看url是不是创建成功,然后下一步看浏览器是不是跳转了。事实上,这样是很费时费力的,目前我们需要解决的问题是: 我们给一个window对象的location属性设置值之后,验证其正确性,而不是浏览器做出来什么响应,并且是能使用单元测试不要使用UI测试(测试金字塔)。 怎么解决? 为window对象注入一个stub。
在正式开始前,我们还需要了解什么是依赖注入: 依赖注入是用测试替身代替依赖的一种流行、通用的技术。也就是说依赖是作为一个参数传递给函数的,而不是直接在函数中引用的。
5、交互测试
什么是交互测试? 在了解交互测试之前,我们先回顾一下之前为同步和异步函数编写的测试,给定特定的参数,它们的预期值始终是不变的,我们把这些测试称为经验测试 ~ 结果是确定的、可预测的,且很容易断定的。 但当我们涉及依赖时,其结果是很难预测的。在编写测试时,需要将注意力放在函数的行为,而不是函数依赖对象是否正确。也就是说我们不需要管这个对象是真是假,只需要检查代码能够以正确的方式与依赖对象进行交互就好了。当函数的依赖成功执行或执行失败时,函数是否进行了正确的处理。我们称这为交互测试 ~ 代码中有很复杂的依赖关系,而且依赖让代码不确定、难以预测、脆弱或耗时。
接下来将使用交互测试为locate函数编写测试用例了。在locate函数中,我们依赖一个异步函数getCurrentPosition去获取当前位置,需要注意的是我们无法确定“当前”位置的准确度,不能依赖这个结果,故根据上边的总结得出我们就没有必要采取经验测试了。
最终,我们需要使用交互测试,专注验证locate函数与getCurrentPosition函数的交互行为,而不是验证getCurrentPosition函数最终返回了正确的位置。因为是为了验证locate函数是否调用了它依赖的函数,所以使用测试替身mock来代替getCurrentPosition函数。但为啥没有使用依赖注入呢?这是因为navigator的属性比location的属性更容易模拟。我们首先复制了getCurrentPosition函数,然后给了它一个模拟的函数,最后又将其还原。这个方法的好处就是让测试变得快速,且可预测。并且我们不用处理是否允许浏览器获取用户位置的问题。
在模拟函数中检查传入的两个参数是否是对onSuccess和onError函数的引用。会存在三种情况: 1、调用了getCurrentPosition函数,并传递了预期的回调函数,测试通过; 2、调用了getCurrentPosition函数,没有传递了预期的回调函数,测试失败; 3、没有调用getCurrentPosition函数,测试失败。
0x02 总结
通过我自己的学习与实践,得知道理都懂,还是需要多练多看,不然还是不知道如何下手。在真正学会测试驱动代码设计之后呢,就需要学会如何识别‘坏味道’即重构手法,这也是需要平时的积累。最后就是去学习能提高效率的指法和快捷键的使用。
最后,接下来根据自己的计划,需要学习数据结构了,同时也需要进一步去实践单元测试,之后至少会出一篇 Node.js 的单元测试策略和一篇 React 的单元测试策略的文章。
0x03 参考
- 《JavaScript测试驱动开发》
- Jest 官网
代码仓库:https://github.com/yihaimen/JestJsApp B 站链接:https://space.bilibili.com/383362014 博客地址:https://yihaimen.github.io/