天天看點

記一次簡單的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, , , );

    assert(callback.calledOn(obj));
    assert(callback.calledWith(, , ));
});
複制代碼
           
行為

once傳回的函數需要傳回源函數的傳回。為了測試這個,我們建立一個假行為:

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

    assert.equals(proxy(), );
});
複制代碼
           

同樣還有 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之間的差別是什麼?

為什麼以spec.js命名?

SO上有這樣一個問題:What does “spec” mean in Javascript Testing

spec是sepcification的縮寫。

就測試而言,Specification指的是給定特性或者必須滿足的應用的技術細節。最好的了解這個問題的方式是:讓某一部分代碼成功通過必須滿足的規範。

如何為聊天的文字消息元件寫單元測試?

運作在哪個檔案夾下?

test檔案夾下即可,檔案名以.spec.js結尾即可,通過files和preprocessors中的配置可以比對到。

karma.conf.js怎麼看?

看不懂karma.conf.js,到 karma-runner.github.io/0.13/config… 學習配置。

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.org/guides/testing-single-file-components-with-karma.html 添加切合vue項目的karma配置。

demo位址:github.com/eddyerburgh…

人生中第一次單元測試

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

測試結果:

意外收獲

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();
    }, );
});
複制代碼
           
phantomjs foo.js
複制代碼
           

運作上述代碼後,會生成一張圖檔,但是畫質感人。

2.karma-webpack是什麼? 在karma中用webpack預處理檔案。

istanbul是什麼?

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文檔檢視:vue-test-utils.vuejs.org/api/options…

mount和shallowMount的差別是什麼?

mount僅僅挂載目前元件執行個體;而shallowMount挂載目前元件執行個體以外,還會挂載子元件。

前端的單元測試,到底該測什麼?

這是一個一直困擾我的問題。 測試通用業務元件?業務變更快速,單元測試波動較大。❌ 測試使用者行為?使用者行為存在上下文關系,組合起來是一個很恐怖的數字,這個交給測試人員去測就好了。❌ 那我到底該測什麼呢?要測試功能型元件,vue插件,二次封裝的庫。✔️

就拿我負責的項目來說:

  • 功能型元件:可複用的上傳元件,可編輯單元格元件,時間選擇元件。(前兩個元件都是老大寫的,第三個是我實踐中抽離出來的。)
  • vue插件:mqtt.js,eventbus.js。(這兩個元件是我抽象的。)
  • 二次封裝庫:httpclient.js。(基于axios,老大初始化,我添磚加瓦。)

上述适用于單元測試的内容都有一個共同點:複用性高!

是以我們在糾結要不要寫單元測試時,抓住複用性高這個特點去考慮就好了。

單元測試是為了保證什麼呢?

  • 按照預期輸入,元件或者庫有預期輸出,告訴開發者all is well。
  • 未按照預期輸入,元件或者庫給出預期提醒,告訴開發者something is wrong。

是以,其實單元測試是為了幫助開發者的突破自己内心的最後一道心理障礙,建立老子的代碼完全ojbk,不可能出問題的自信。

其實最終還是保證使用者有無bug的元件可用,有好的軟體或平台使用,讓自己的生活變得更加美好。

前端的單元測試,到底該測什麼?

這是一個一直困擾我的問題。 測試通用業務元件?業務變更快速,單元測試波動較大。❌ 測試使用者行為?使用者行為存在上下文關系,組合起來是一個很恐怖的數字,這個交給測試人員去測就好了。❌ 那我到底該測什麼呢?要測試功能型元件,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();
    wrapper.vm.$bus.$emit('foo', '$bus emitted an foo event');
    wrapper.vm.$bus.$emit('bar', '$bus emitted an bar event');
  });
});

複制代碼