记一次简单的vue组件单元测试

简介: 记一次简单的vue组件单元测试

记录一些在为项目引入单元测试时的一些困惑,希望可以对社区的小伙伴们有所启迪,少走一些弯路少踩一些坑。

  • jest, mocha, karma, chai, sinon, jsmine, vue-test-utils都是些什么东西?
  • chai,sinon是什么?
  • 为什么以spec.js命名?
  • 如何为聊天的文字消息组件写单元测试?
  • 运行在哪个目录下?
  • karma.conf.js怎么看?
  • 人生中第一次单元测试
  • istanbul是什么?
  • vue-test-utils的常用api?
  • 前端的单元测试,到底该测什么?


jest, mocha, karma, chai, sinon, jsmine, vue-test-utils都是些什么东西?


名词 Github描述 个人理解
jest Delightful JavaScript Testing. Works out of the box for any React project.Capture snapshots of React trees facebook家的测试框架,与react打配合会更加得心应手一些。
mocha Simple, flexible, fun JavaScript test framework for Node.js & The Browser 强大的测试框架,中文名叫抹茶,常见的describe,beforeEach就来自这里
karma A simple tool that allows you to execute JavaScript code in multiple real browsers. Karma is not a testing framework, nor an assertion library. Karma just launches an HTTP server, and generates the test runner HTML file you probably already know from your favourite testing framework. 不是测试框架,也不是断言库,可以做到抹平浏览器障碍式的生成测试结果
chai BDD / TDD assertion framework for node.js and the browser that can be paired with any testing framework. BDD/TDD断言库,assert,expect,should比较有趣
sinon Standalone and test framework agnostic JavaScript test spies, stubs and mocks js mock测试框架,everything is fake,spy比较有趣
jsmine Jasmine is a Behavior Driven Development testing framework for JavaScript. It does not rely on browsers, DOM, or any JavaScript framework. Thus it's suited for websites, Node.js projects, or anywhere that JavaScript can run. js BDD测试框架
vue/test-utils Utilities for testing Vue components 专门为测试单文件组件而开发,学会使用vue-test-utils,将会在对vue的理解上更上一层楼

通过上述的表格,作为一个vue项目,引入单元测试,大致思路已经有了:

测试框架:mocha
抹平环境:karma
断言库:chai
BDD库:jsmine


这并不是最终结果,测试vue单文件组件,当然少不了vue-test-utils,但是将它放在什么位置呢。

需要阅读vue-test-utils源码。


chai,sinon是什么?


chai是什么?

  • Chai是一个node和浏览器可用的BDD/TDD断言库。
  • Chai类似于Node内建API的assert。
  • 三种常用风格:assert,expect或者should。


const chai = require('chai');
const assert = chai.assert;
const expect = chai.expect();
const should = chai.should();


sinon是什么?

  • 一个 once函数,该如何测试这个函数?
  • spy是什么?


function once(fn) {
    var returnValue, called = false;
    return function () {
        if (!called) {
            called = true;
            returnValue = fn.apply(this, arguments);
        }
        return returnValue;
    };
}
Fakes
it('calls the original function', function () {
    var callback = sinon.fake();
    var proxy = once(callback);
    proxy();
    assert(callback.called);
});

只调用一次更重要:

it('calls the original function only once', function () {
    var callback = sinon.fake();
    var proxy = once(callback);
    proxy();
    proxy();
    assert(callback.calledOnce);
    // ...or:
    // assert.equals(callback.callCount, 1);
});

而且我们同样觉得this和参数重要:

it('calls original function with right this and args', function () {
    var callback = sinon.fake();
    var proxy = once(callback);
    var obj = {};
    proxy.call(obj, 1, 2, 3);
    assert(callback.calledOn(obj));
    assert(callback.calledWith(1, 2, 3));
});


行为

once返回的函数需要返回源函数的返回。为了测试这个,我们创建一个假行为:


it("returns the return value from the original function", function () {
    var callback = sinon.fake.returns(42);
    var proxy = once(callback);
    assert.equals(proxy(), 42);
});


同样还有 Testing Ajax,Fake XMLHttpRequest,Fake server,Faking time等等。


sinon.spy()?


test spy是一个函数,它记录所有的参数,返回值,this值和函数调用抛出的异常。

有3类spy:

  • 匿名函数
  • 具名函数
  • 对象的方法


匿名函数


测试函数如何处理一个callback。

"test should call subscribers on publish": function() {
    var callback = sinon.spy();
    PubSub.subscribe("message", callback);
    PubSub.publishSync("message");
    assertTrue(callback.called);
}


对象的方法

用spy包裹一个存在的方法。

sinon.spy(object,"method")创建了一个包裹了已经存在的方法object.method的spy。这个spy会和源方法一样表现(包括用作构造函数时也是如此),但是你可以拥有数据调用的所有权限,用object.method.restore()可以释放出spy。这里有一个人为的例子:

{
    setUp: function () {
        sinon.spy(jQuery, "ajax");
    },
    tearDown: function () {
        jQuery.ajax.restore();// 释放出spy
    },
}


引申问题

BDD/TDD是什么?

What’s the difference between Unit Testing, TDD and BDD?

[[译]单元测试,TDD和BDD之间的区别是什么?](https://github.com/FrankKai/F...


为什么以spec.js命名?


SO上有这样一个问题:What does “spec” mean in Javascript Testing

spec是sepcification的缩写。

就测试而言,Specification指的是给定特性或者必须满足的应用的技术细节。最好的理解这个问题的方式是:让某一部分代码成功通过必须满足的规范。


如何为聊天的文字消息组件写单元测试?


运行在哪个文件夹下?

test文件夹下即可,文件名以.spec.js结尾即可,通过files和preprocessors中的配置可以匹配到。

karma.conf.js怎么看?

看不懂karma.conf.js,到 http://karma-runner.github.io... 学习配置。


const webpackConfig = require('../../build/webpack.test.conf');
module.exports = function karmaConfig(config) {
  config.set({
    browsers: ['PhantomJS'],// Chrome,ChromeCanary,PhantomJS,Firefox,Opera,IE,Safari,Chrome和PhantomJS已经在karma中内置,其余需要插件
    frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],// ['jasmine','mocha','qunit']等等,需要额外通过NPM安装
    reporters: ['spec', 'coverage'],//  默认值为progress,也可以是dots;growl,junit,teamcity或者coverage需要插件。spec需要安装karma-spec-reporter插件。
    files: ['./index.js'],// 浏览器加载的文件,  `'test/unit/*.spec.js',`等价于 `{pattern: 'test/unit/*.spec.js', watched: true, served: true, included: true}`。
    preprocessors: {
      './index.js': ['webpack', 'sourcemap'],// 预处理加载的文件
    },
    webpack: webpackConfig,// webpack配置,karma会自动监控test的entry points
    webpackMiddleware: {
      noInfo: true, // webpack-dev-middleware配置
    },
    // 配置reporter 
    coverageReporter: {
      dir: './coverage',
      reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }],
    },
  });
};


结合实际情况,通过https://vue-test-utils.vuejs.... 添加切合vue项目的karma配置。

demo地址:https://github.com/eddyerburg...


人生中第一次单元测试


karma.conf.js

// This is a karma config file. For more details see
//   http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
//   https://github.com/webpack/karma-webpack
const webpackConfig = require('../../build/webpack.test.conf');
module.exports = function karmaConfig(config) {
  config.set({
    // to run in additional browsers:
    // 1. install corresponding karma launcher
    //    http://karma-runner.github.io/0.13/config/browsers.html
    // 2. add it to the `browsers` array below.
    browsers: ['Chrome'],
    frameworks: ['mocha'],
    reporters: ['spec', 'coverage'],
    files: ['./specs/*.spec.js'],
    preprocessors: {
      '**/*.spec.js': ['webpack', 'sourcemap'],
    },
    webpack: webpackConfig,
    webpackMiddleware: {
      noInfo: true,
    },
    coverageReporter: {
      dir: './coverage',
      reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }],
    },
  });
};


test/unit/specs/chat.spec.js


import { mount } from '@vue/test-utils';
import { expect } from 'chai';
import ChatText from '@/pages/chat/chatroom/view/components/text';
describe('ChatText.vue', () => {
  it('人生中第一次单元测试:', () => {
    const wrapper = mount(ChatText);
    console.log(wrapper.html());
    const subfix = '</p> <p>默认文字</p></div>';
    expect(wrapper.html()).contain(subfix);
  });
});


注意,被测试组件必须以index.js暴露出组件。

NODE_ENV=testing karma start test/unit/karma.conf.js --single-run

测试结果:


image.png


意外收获


1.PhantomJS是什么?

  • 是一个无头的脚本化浏览器。
  • 可以运行在Windows, macOS, Linux, and FreeBSD.
  • QtWebKit,可以做DOM处理,可以CSS选择器,可以JSON,可以Canvas,也可以SVG。

下载好phantomjs后,就可以在终端中模拟浏览器操作了。

foo.js


var page = require('webpage').create();
page.open('http://www.google.com', function() {
    setTimeout(function() {
        page.render('google.png');
        phantom.exit();
    }, 200);
});



phantomjs foo.js


image.png

2.karma-webpack是什么?

在karma中用webpack预处理文件。


istanbul是什么?


image.png

vue-test-utils的常用api及其option?


  • mount:propsData,attachToDocument,slots,mocks,stubs?
  • mount和shallowMount的区别是什么?

啥玩意儿???一一搞定。


mount:propsData,attachToDocument,slots,mocks,stubs?


this.vm.$options.propsData // 组件的自定义属性,是因为2.1.x版本中没有$props对象,https://vue-test-utils.vuejs.org/zh/api/wrapper/#setprops-props
const elm = options.attachToDocument ? createElement() : undefined // "<div>" or undefined
slots // 传递一个slots对象到组件,用来测试slot是否生效的,值可以是组件,组件数组或者字符串,key是slot的name
mocks // 模拟全局注入
stubs // 存根子组件


后知后觉,这些都可以在Mounting Options文档查看:https://vue-test-utils.vuejs....

mount和shallowMount的区别是什么?

mount仅仅挂载当前组件实例;而shallowMount挂载当前组件实例以外,还会挂载子组件。


前端的单元测试,到底该测什么?


这是一个一直困扰我的问题。

测试通用业务组件?业务变更快速,单元测试波动较大。❌

测试用户行为?用户行为存在上下文关系,组合起来是一个很恐怖的数字,这个交给测试人员去测就好了。❌

那我到底该测什么呢?要测试功能型组件,vue插件,二次封装的库。✔️

就拿我负责的项目来说:

功能型组件:可复用的上传组件,可编辑单元格组件,时间选择组件。(前两个组件都是老大写的,第三个是我实践中抽离出来的。)vue插件:mqtt.js,eventbus.js。(这两个组件是我抽象的。)二次封装库:httpclient.js。(基于axios,老大初始化,我添砖加瓦。)

上述适用于单元测试的内容都有一个共同点:复用性高!


所以我们在纠结要不要写单元测试时,抓住复用性高这个特点去考虑就好了。

单元测试是为了保证什么呢?


  • 按照预期输入,组件或者库有预期输出,告诉开发者all is well。
  • 未按照预期输入,组件或者库给出预期提醒,告诉开发者something is wrong。

所以,其实单元测试是为了帮助开发者的突破自己内心的最后一道心理障碍,建立老子的代码完全ojbk,不可能出问题的自信。


其实最终还是保证用户有无bug的组件可用,有好的软件或平台使用,让自己的生活变得更加美好。


如何为vue插件 eventbus 写单元测试?

/*
  title: vue插件eventbus单测
  author:frankkai
  target: 1.Vue.use(eventBus)是否可以正确注入$bus到prototype
          2.注入的$bus是否可以成功挂载到组件实例
          3.$bus是否可以正常订阅消息($on)和广播消息($emit)
 */
import eventbusPlugin from '@/plugins/bus';
import { createLocalVue, createWrapper } from '@vue/test-utils';
import { expect } from 'chai';
const localVue = createLocalVue();
localVue.use(eventbusPlugin);
const localVueInstance = (() =>
  localVue.component('empty-vue-component', {
    render(createElement) {
      return createElement('div');
    },
  }))();
const Constructor = localVue.extend(localVueInstance);
const vm = new Constructor().$mount();
const wrapper = createWrapper(vm);
describe('/plugins/bus.js', () => {
  it('Vue.use(eventBus)是否可以正确注入$bus到prototype:', () => {
    expect('$bus' in localVue.prototype).to.equal(true);
  });
  it('注入的$bus是否可以成功挂载到组件实例:', () => {
    expect('$bus' in wrapper.vm).to.equal(true);
  });
  it('$bus是否可以正常订阅消息($on)和广播消息($emit):', () => {
    wrapper.vm.$bus.$on('foo', (payload) => {
      expect(payload).to.equal('$bus emitted an foo event');
    });
    wrapper.vm.$bus.$on('bar', (payload) => {
      expect(payload).to.equal('$bus emitted an bar event');
    });
    expect(Object.keys(vm.$bus._events).length).to.equal(2);
    wrapper.vm.$bus.$emit('foo', '$bus emitted an foo event');
    wrapper.vm.$bus.$emit('bar', '$bus emitted an bar event');
  });
});


image.png

相关文章
|
3月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
510 0
|
2天前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
34 1
|
3月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
5月前
|
JavaScript
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
712 4
|
3月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件的实现代码:支持自定义表情库、快捷键发送和输入框联动的聊天表情解决方案
本文详细介绍了在 Vue 项目中实现一个功能完善、交互友好的表情包输入组件的方法,并提供了具体的应用实例。组件设计包含表情分类展示、响应式布局、与输入框的交互及样式定制等功能。通过核心技术实现,如将表情插入输入框光标位置和点击外部关闭选择器,确保用户体验流畅。同时探讨了性能优化策略,如懒加载和虚拟滚动,以及扩展性方案,如自定义主题和国际化支持。最终,展示了如何在聊天界面中集成该组件,为用户提供丰富的表情输入体验。
301 8
|
3月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
189 1
|
3月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
122 0
|
5月前
|
存储 JavaScript 前端开发
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
392 6
|
10月前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
237 64
|
10月前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
332 64