实例入门 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单元测试
- 单元测试 – 我应该对不应该在函数中传递的数据(无效输入)进行单元测试吗?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Algorithms for Image Processing and Computer Vision
Parker, J. R. / 2010-12 / 687.00元
A cookbook of algorithms for common image processing applications Thanks to advances in computer hardware and software, algorithms have been developed that support sophisticated image processing with......一起来看看 《Algorithms for Image Processing and Computer Vision》 这本书的介绍吧!