实例入门 Vue.js 单元测试
栏目: JavaScript · 发布时间: 6年前
内容简介:作为一个以那是什么原因让 Vue.js 的开发团队如此重视单元测试,要在这个同样以官方文档中给出了非常清楚的说法:
作为一个以 文档丰富 而广为人知的前端开发框架,Vue.js 的官方文档中分别在《教程-工具-单元测试》、《Cookbook-Vue组件的单元测试》里对 Vue 组件的单元测试方法做出了介绍,并提供了官方的单元测试实用 工具 库Vue Test Utils;甚至在状态管理工具Vuex 的文档里也不忘留出《测试》一章。
那是什么原因让 Vue.js 的开发团队如此重视单元测试,要在这个同样以 易于上手 为卖点的框架中大力科普呢?
官方文档中给出了非常清楚的说法:
组件的单元测试有很多好处: - 提供描述组件行为的文档 - 节省手动测试的时间 - 减少研发新特性时产生的 bug - 改进设计 - 促进重构 自动化测试使得大团队中的开发者可以维护复杂的基础代码。 复制代码
本文作为 《对 React 组件进行单元测试》 一文的姊妹篇,将照猫画虎式的尝试面对初学和向中级进阶的开发者,对单元测试在 Vue.js 技术栈 中的应用做出入门介绍。
I. 单元测试简介
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
简单来说, 单元
就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
讲解具体概念之前,先咀个栗子直观了解下:
比如我们有这样一个模块,暴露两个方法用以对菜单路径进行一些处理:
// src/menuChecker.js export function getRoutePath(str) { let to = "" //... return to; } export function getHighlight(str) { let hl = ""; //... return hl; } 复制代码
编写对应的测试文件:
import { getRoutePath, getHighlight } from "@/menuChecker"; describe("检查菜单路径相关函数", ()=>{ it("应该获得正确高亮值", ()=>{ expect( getHighlight("/myworksheet/(.*)") ).toBe("myTickets"); }); it("应该为未知路径取得默认的高亮值", ()=>{ expect( getHighlight("/myworksheet/ccc/aaa") ).toBe("mydefaulthl111"); }); it("应该补齐开头的斜杠", ()=>{ expect( getRoutePath("/worksheet/list") ).toBe('/worksheet/list'); }); it("应该能修正非法的路径", ()=>{ expect( getRoutePath("/myworksheet/(.*)") ).toBe("/myworksheet/list"); }); }); 复制代码
运行该测试文件,得到如下输出:
运行结果可以说非常友好了,虽然醒目的提示了 FAIL ,但是哪条判断错了、错在哪一行、实际的返回值与预期的区别,甚至代码覆盖率的表格,都分别展示了出来;尤其是最重要的对错结果,分别用绿色红色加以展示。
真相只有一个,要么是目标模块写的有问题,要么是测试条件写错了 -- 总之我们对其修正后重新运行:
由此,我们对一次单元测试的过程有了基本的了解。
首先,对所谓“单元”的定义是灵活的,可以是一个函数,可以是一个模块,也可以是一个 Vue Component。
其次,由于测试结果中,成功的用例会用绿色表示,而失败的部分会显示为红色,所以单元测试也常常被称为 “Red/Green Testing” 或 “Red/Green Refactoring”,其一般步骤可以归纳为:
- 添加一个测试
- 运行所有测试,看看新加的这个测试是不是失败了;如果能成功则重复步骤1
- 根据失败报错,有针对性的编写或改写代码;这一步的唯一目的就是通过测试,先不必纠结细节
- 再次运行测试;如果能成功则跳到步骤5,否则重复步骤3
- 重构已经通过测试的代码,使其更可读、更易维护,且不影响通过测试
- 重复步骤1,直到所有功能测试完毕
1.1 测试框架
测试框架的作用是提供一些方便的语法来描述测试用例,以及对用例进行分组。
1.2 断言(assertions)
断言是单元测试框架中核心的部分,断言失败会导致测试不通过,或报告错误信息。
对于常见的断言,举一些例子如下:
-
同等性断言 Equality Asserts
- expect(sth).toEqual(value)
- expect(sth).not.toEqual(value)
-
比较性断言 Comparison Asserts
- expect(sth).toBeGreaterThan(number)
- expect(sth).toBeLessThanOrEqual(number)
-
类型性断言 Type Asserts
- expect(sth).toBeInstanceOf(Class)
-
条件性测试 Condition Test
- expect(sth).toBeTruthy()
- expect(sth).toBeFalsy()
- expect(sth).toBeDefined()
1.3 断言库
断言库主要提供上述断言的语义化方法,用于对参与测试的值做各种各样的判断。这些语义化方法会返回测试的结果,要么成功、要么失败。常见的断言库有 Should.js, Chai.js 等。
1.4 测试用例 test case
为某个特殊目标而编制的一组测试输入、执行条件以及预期结果,以便测试某个程序路径或核实是否满足某个特定需求。
一般的形式为:
it('should ...', function() { ... expect(sth).toEqual(sth); }); 复制代码
1.5 测试套件 test suite
通常把一组相关的测试称为一个测试套件
一般的形式为:
describe('test ...', function() { it('should ...', function() { ... }); it('should ...', function() { ... }); ... }); 复制代码
1.6 spy
正如 spy
字面的意思一样,我们用这种“间谍”来“监视”函数的调用情况
通过对监视的函数进行包装,可以通过它清楚的知道该函数被调用过几次、传入什么参数、返回什么结果,甚至是抛出的异常情况。
var spy = sinon.spy(MyComp.prototype, 'someMethod'); ... expect(spy.callCount).toEqual(1); 复制代码
1.7 stub
有时候会使用 stub
来嵌入或者直接替换掉一些代码,来达到隔离的目的
一个 stub
可以使用最少的依赖方法来模拟该单元测试。比如一个方法可能依赖另一个方法的执行,而后者对我们来说是透明的。好的做法是使用stub 对它进行隔离替换。这样就实现了更准确的单元测试。
var myObj = { prop: function() { return 'foo'; } }; sinon.stub(myObj, 'prop').callsFake(function() { return 'bar'; }); myObj.prop(); // 'bar' 复制代码
1.8 mock
mock
一般指在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法
广义的讲,以上的 spy 和 stub 等,以及一些对模块的模拟,对 ajax 返回值的模拟、对 timer 的模拟,都叫做 mock 。
1.9 测试覆盖率(code coverage)
用于统计测试用例对代码的测试情况,生成相应的报表,比如 istanbul
是常见的测试覆盖率统计工具。
istanbul
也就是土耳其首都 “伊斯坦布尔”,这样命名是因为土耳其地毯世界闻名,而地毯是用来"覆盖"的:mask:。
回顾一下上面的图:
表格中的第2列至第5列,分别对应了四个衡量维度:
if
测试结果根据覆盖率被分为“绿色、黄色、红色”三种,应该关注这些指标,测试越全面,就能提供更高的保证。
同时也没有必要一味追求行覆盖率,因为它会导致我们过分关注组件的内部实现细节,从而导致琐碎的测试。
II. Vue.js 中的单元测试工具
2.1 Jest
不同于"传统的"(其实也没出现几年)的 jasmine / Mocha / Chai 等前端测试框架; Jest
的使用更简单(也许就是这个单词的本意“俏皮话、玩笑话”的意思),并且提供了更高的集成度、更丰富的功能。
Jest 是一个由 Facebook 开发的测试运行器,相对其他测试框架,其特点就是就是内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用。
此外, Jest 的测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升了测试速度。
配置
Jest 号称自己是一个 “Zero configuration testing platform”,只需在 npm scripts
里面配置了 test: jest
,即可运行 npm test
,自动识别并测试符合其规则的( Vue.js 项目中一般是 __tests__
目录下的)用例文件。
实际使用中,适当的在 package.json 的 jest 字段或独立的 jest.config.js 里自定义配置一下,会得到更适合我们的测试场景。
参考文档 vue-test-utils.vuejs.org/zh/guides/t… ,可以很快在 Vue.js 项目中配置好 Jest 测试环境。
四个基础单词
编写单元测试的语法通常非常简单;对于 jest
来说,由于其内部使用了 Jasmine 2
来进行测试,故其用例语法与 Jasmine 相同。
实际上,只要先记这住四个单词,就足以应付大多数测试情况了:
describe it expect toEqual
describe('test ...', function() { it('should ...', function() { expect(sth).toEqual(sth); expect(sth.length).toEqual(1); expect(sth > oth).toEqual(true); }); }); 复制代码
2.2 sinon
图中这位“我牵着马”的并不是卷帘大将沙悟净...其实图中的故事正是人所皆知的“特洛伊木马”;大概意思就是希腊人围困了特洛伊人十多年,久攻不下,心生一计,把营盘都撤了,只留下一个巨大的木马(里面装着士兵),以及这位被扒光还被打得够呛的人,也就是此处要谈的主角 sinon,由他欺骗特洛伊人 --- 后面的剧情大家就都熟悉了。
所以这个命名的测试工具呢,也正是各种伪装渗透方法的合集,为单元测试提供了独立而丰富的 spy, stub 和 mock 方法,兼容各种测试框架。
虽然 Jest 本身也有一些实现 spy 等的手段,但 sinon 使用起来更加方便。
2.3 Vue Test Utils
Vue Test Utils 是 Vue.js 官方的单元测试实用工具库;该工具库使用起来和用以测试 React 组件的 Enzyme 工具库非常相似
它模拟了一部分类似 jQuery 的 API,非常直观并且易于使用和学习,提供了一些接口和几个方法来减少测试的样板代码,方便判断、操纵和遍历 Vue Component 的输出,并且减少了测试代码和实现代码之间的耦合。
一般使用其 mount()
或 shallowMount()
方法,将目标组件转化为一个 Wrapper
对象,并在测试中调用其各种方法,例如:
import { mount } from '@vue/test-utils' import Foo from './Foo.vue' describe('Foo', () => { it('renders a div', () => { const wrapper = mount(Foo) expect(wrapper.contains('div')).toBe(true) }) }) 复制代码
III. 一个 Vue.js 的单元测试实例
3.1 又一个栗子
import { shallowMount } from "@vue/test-utils"; import Vue from 'vue'; import VueI18n from 'vue-i18n'; import i18nMessage from '@/i18n'; import Comp from "@/components/Device.vue"; const fakeData = { //假数据 deviceNo: "abcdefg", deviceSpace: 45, deviceStatus: 2, devices: [ { id: "test001", location: "12", status: 1 }, { id: "test002", location: "58", status: 3 }, { id: "test003", location: "199", status: 4 } ] }; Vue.use(VueI18n); //重现必要的依赖 const i18n = new VueI18n({ locale: 'zh-CN', silentTranslationWarn: true, missing: (locale, key, vm) => key, messages: i18nMessage }); let wrapper = null; const makeWrapper = ()=>{ wrapper = shallowMount( Comp, { i18n, //看这里 propsData: { //还有这里 unitHeight: 5, data: fakeData } } ); }; afterEach(()=>{ //也很常见的用法 if (!wrapper) return; wrapper = null; }); describe("test Device.vue", ()=>{ it("should be a VUE instance", ()=>{ makeWrapper(); expect( wrapper.isVueInstance() ).toBeTruthy(); }); it("应该有正常的总高度", ()=>{ makeWrapper(); expect( wrapper.vm.totalHeight ).toBe( 1230 ); }); it("应该渲染正确的设备数量", ()=>{ makeWrapper(); expect( wrapper.findAll('.deviceitem').length ).toBe( 3 ); }); it("指定的设备应该在正确的位置", ()=>{ makeWrapper(); const sty = wrapper.findAll('.deviceitem').at(1).attributes('style'); expect( sty ).toMatch( /height\:\s*20px/ ); expect( sty ).toMatch( /bottom\:\s*20px/ ); }); it("应该渲染正确的tooltip", ()=>{ makeWrapper(); //这里的用法值得注意 const popper_ref = wrapper.find({ref: 'device_tooltip_test002'}); expect( popper_ref.exists() ).toBeTruthy(); const cont = popper_ref.find('.tooltip_cont'); expect( cont.html() ).toMatch(/所在位置\:\s58/); }); it("应该渲染正确的设备分类", ()=>{ makeWrapper(); const badge = wrapper.find('.badge'); expect( badge.exists() ).toBeTruthy(); expect( badge.findAll('li').length ).toBe(4); expect( badge.findAll('li').at(2).text() ).toBe('喷雾设备'); }); it("当点击了关闭按钮,应该不再显示", (done)=>{ //异步的用例 makeWrapper(); wrapper.vm.$nextTick(()=>{ //再看这里 expect( wrapper.find('.devices_container').exists() ).toBeFalsy(); done(); }); }); }); 复制代码
这里无需逐条的解释,主要的 API 在 Jest
和 Vue Test Utils
的文档里都能找到。
其中值得注意的小经验,一是一些异步更新(比如代码中有延时)后正确使用 wrapper.vm.$nextTick
;二是对于一些挂载到 document.body 等外部位置的组件元素,要靠 wrapper.find({ref: xxx})
取得其引用。
3.2 整合到工作流中
写好的单元测试,如果仅仅要靠每次 npm test
手动执行,必然会有日久忘记、逐渐过时,最后甚至无法执行的情况。
有多个时间点可以作为选择,插入自动执行单元测试 -- 例如每次保存文件、每次执行 build 等;此处我们选择了一种很简单的配置办法:
首先在项目中安装 pre-commit
依赖包;然后在 package.json
中配置 npm scripts :
"scripts": { ... "test": "jest" }, "pre-commit": [ "test" ], 复制代码
这样在每次 git commit
之前,项目中存在的单元测试就会自动执行一次,往往就避免了 “改一个 bug,送十个新 bug” 的窘况。
IV. 用单元测试改善 Vue.js 组件
单元测试除了减少错误,另一个显著的好处是能让我们组件化的思路越来越清晰,养成日益良好的习惯。
一个被验证过针对给定的输入会渲染出符合期望的输出的组件,称为 测试通过的 组件;
一个 可测试的(testable) 组件意味着其易于测试
如何确保一个组件如期望的工作呢?
我们可能习惯于依靠双手和眼睛,一次次的验证我们写过的组件;但如果你打算对每个组件的每个改动都手动验证的话,或早或晚就会因为疲惫或懈怠,导致瑕疵留在代码中。
这就是自动化的单元测试为何重要的原因。单元测试保证了每次对组件做出的更改后,组件都能正确工作。
单元测试并不只与早期发现 bug 有关。另一个重要的方面是用其检验组件架构化水平优劣的能力。
一个 无法测试 或 难以测试 的组件,基本上就等同于 设计得很拙劣 的组件.
组件之所以难以测试,是因为其有太多的 props、依赖、引用的模型和对全局变量的访问 -- 这都是不良设计的标志。
一个设计不佳的组件,就会变成无法测试的,进而你就会简单的跳过单元测试,又导致了其保持未测试状态,变成一个恶性循环。
4.1 希望是最后一个栗子
假设要对 NumStepper.vue 组件进行测试
//NumStepper.vue <template> <div> <button class="plus" v-on:click="updateNumber(+1)">加</button> <button class="minus" v-on:click="updateNumber(-1)">减</button> <button class="zero" v-on:click="clear">清</button> </div> </template> <script> export default { props: { targetData: Object, clear: Function }, methods: { updateNumber: function(n) { this.targetData.num += n; } } } </script> 复制代码
该组件又依赖一个外层组件给其提供数据和方法:
//NumberDisplay.vue <template> <div> <p>{{somedata.num}}</p> <NumStepper :targetData="somedata" :clear="clear" /> </div> </template> <script> import NumStepper from "./NumStepper" export default { components: { NumStepper }, data() { return { somedata: { num: 999 }, tgt: this } }, methods: { clear: function() { this.somedata.num = 0; } } } </script> 复制代码
这样一来,我们的测试就得这样写:
import { shallowMount } from "@vue/test-utils"; import Vue from 'vue'; import NumStepper from '@/components/NumStepper'; import NumberDisplay from '@/components/NumberDisplay'; describe("测试 NumStepper 组件", ()=>{ it("应该能够影响外层组件的数据", ()=>{ const display = shallowMount(NumberDisplay); const wrapper = shallowMount(NumStepper, { propsData: { targetData: display.vm.somedata, clear: display.vm.clear } }); expect(display.vm.somedata.num).toBe(999); wrapper.find('.plus').trigger('click'); wrapper.find('.plus').trigger('click'); expect(display.vm.somedata.num).toBe(1001); wrapper.find('.minus').trigger('click'); expect(display.vm.somedata.num).toBe(1000); wrapper.find('.zero').trigger('click'); expect(display.vm.somedata.num).toBe(0); }) }); 复制代码
<NumStepper>
测试起来非常复杂,因为它关联了外部组件的实现细节。
测试场景中需要一个额外的 <NumberDisplay>
组件,用来重现外部组件、向目标组件传递数据和方法,并检验目标组件是否正确修改了外部组件的状态。
不难想象,加入 <NumberDisplay>
组件再依赖其他组件或环境变量、全局方法等,事情将变得更糟糕,可能需要单独实现若干测试专用组件,甚至根本无法测试。
4.2 真正的最后一个栗子
当 <NumStepper>
独立于外部组件的细节时,测试就简单了。让我们实现并测试一下合理封装版本的 <NumStepper>
组件:
//NumStepper2.vue <template> <div> <button class="plus" v-on:click="updateFunc(+1)">加</button> <button class="minus" v-on:click="updateFunc(-1)">减</button> <button class="zero" v-on:click="clearFunc">清</button> </div> </template> <script> export default { props: { updateFunc: Function, clearFunc: Function } } </script> 复制代码
在测试中,就不用引入额外的组件了:
import { shallowMount } from "@vue/test-utils"; import Vue from 'vue'; import NumStepper from '@/components/NumStepper2'; describe("测试 NumStepper 组件", ()=>{ it("应该能够影响外层组件的数据", ()=>{ const obj = { func1: function(){}, func2: function(){} }; const spy1 = jest.spyOn(obj, "func1"); const spy2 = jest.spyOn(obj, "func2"); const wrapper = shallowMount(NumStepper, { propsData: { updateFunc: spy1, clearFunc: spy2 } }); wrapper.find('.plus').trigger('click'); expect(spy1).toHaveBeenCalled(); wrapper.find('.minus').trigger('click'); expect(spy1).toHaveBeenCalled(); wrapper.find('.zero').trigger('click'); expect(spy2).toHaveBeenCalled(); }) }); 复制代码
注:该示例中只是检验了是否被点击,还可以引入 sinon 的相关方法检验传入的参数等,写出更完备的测试。
V. 总结
单元测试作为一种经典的开发和重构手段,在软件开发领域被广泛认可和采用;前端领域也逐渐积累起了丰富的测试框架和方法。
单元测试可以为我们的开发和维护提供基础保障,使我们在思路清晰、心中有底的情况下完成对代码的搭建和重构。
封装好则测试易,反之不恰当的封装让测试变得困难。
可测试性是一个检验组件结构良好程度的实践标准。
以上所述就是小编给大家介绍的《实例入门 Vue.js 单元测试》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 学习 Node.js,第 9 单元:单元测试
- Vue 应用单元测试的策略与实践 02 - 单元测试基础
- Vue 应用单元测试的策略与实践 04 - Vuex 单元测试
- Vue 应用单元测试的策略与实践 03 - Vue 组件单元测试
- Angular单元测试系列-Component、Directive、Pipe 以及Service单元测试
- 单元测试 – 我应该对不应该在函数中传递的数据(无效输入)进行单元测试吗?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
爆款:如何打造超级IP
【美】安妮塔•埃尔伯斯 / 杨雨 / 中信出版社 / 2016-1-10 / 49
哈佛商学院IP运营与产品管理方法论第一书,翻转长尾理论的重要著作! 电影大片、当红炸子鸡、百万畅销书背后的运营逻辑是什么? 《五十度灰》、Lady Gaga、维多利亚的秘密有何共同秘密? 漫威如何将蜘蛛侠、X战警、绿巨人打造成金矿? 皇马如何打造体育IP,一跃成为全球收 入最高的足球俱乐部? 爆款策略如何运用于电影、电视、音乐、出版、体育与商业各领域? ----......一起来看看 《爆款:如何打造超级IP》 这本书的介绍吧!