記一次簡單的vue元件單元測試

趁你還年輕233發表於2018-12-02

記一次簡單的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之間的區別是什麼?

為什麼以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 測試結果:

image

意外收穫

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
2.karma-webpack是什麼? 在karma中用webpack預處理檔案。

istanbul是什麼?

image

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(2);
    wrapper.vm.$bus.$emit('foo', '$bus emitted an foo event');
    wrapper.vm.$bus.$emit('bar', '$bus emitted an bar event');
  });
});

複製程式碼

記一次簡單的vue元件單元測試

相關文章