天天看點

vue init webpack缺少辨別符_Vue 之五 —— 單元測試

vue init webpack缺少辨別符_Vue 之五 —— 單元測試
單元測試(unit testing):是指對軟體中的最小可測試單元進行檢查和驗證。代碼的終極目标有兩個,第一個是實作需求,第二個是提高代碼品質和可維護性。單元測試是為了提高代碼品質和可維護性,是實作代碼的第二個目标的一種方法。對vue元件的測試是希望元件行為符合我們的預期。

本文将從架構選型,環境搭建,使用方式,vue元件測試編寫原則四個方面講述如何在vue項目中落地單元測試。

一、架構選型

cypress

/

vue-test-utils

選擇

vue-test-utils

是因為它是官方推薦的vue component 單元測試庫。

選擇

cypress

而不是

jest

主要是因為:

  • 測試環境的一緻性 : 在cypress上面跑的測試代碼是在浏覽器環境上的,而非像jest等在node上的。另外由于cypress在浏覽器環境上運作,測試dom相關無需各種mock(如node-canvas等)
  • 統一測試代碼風格、避免技術負擔 : 本身定位 e2e, 但是支援 unit test。
  • 支援CI環境

此外cypress還有很多非常棒的Features,感興趣的朋友自行參考cypress官方文檔。

二、環境搭建

1、安裝依賴

npm i cypress @cypress/webpack-preprocessor start-server-and-test nyc babel-plugin-istanbul @vue/test-utils -D

note: 如果是使用vue cli3建立的項目,可以使用
# vue add @vue/cli-plugin-e2e-cypress
# npm i @cypress/webpack-preprocessor start-server-and-test nyc babel-plugin-istanbul @vue/test-utils -D
           

@cypress/webpack-preprocessor

:引入webpack 預處理器

start-server-and-test

:啟動dev-server 監聽端口啟動成功,再執行測試指令。cypress 需要dev-server啟動才能測試。

nyc babel-plugin-istanbul

:覆寫率統計相關

2、添加/修改cypress.json檔案

{
    "baseUrl": "http://localhost:9001",
    "coverageFolder": "coverage",
    "integrationFolder": "src",
    "testFiles": "**/*.spec.js",
    "video": false,
    "viewportHeight": 900,
    "viewportWidth": 1600,
    "chromeWebSecurity": false
}
           

3、修改package.json配置

"scripts": {
        "cy:run": "cypress run",
        "cy:open": "cypress open",
        "cy:dev": "start-server-and-test start :9001 cy:open",
        "coverage": "nyc report -t=coverage",
        "test": "rm -rf coverage && start-server-and-test start :9001 cy:run && nyc report -t=coverage"
    },
           

4、修改cypress/plugins/index.js(使用vue add @vue/cli-plugin-e2e-cypress的是tests/e2e//plugins/index.js)

// vue cli3 版本
const webpack = require('@cypress/webpack-preprocessor');
const webpackOptions = require('@vue/cli-service/webpack.config');

webpackOptions.module.rules.forEach(rule => {
    if (!Array.isArray(rule.use)) return null;

    rule.use.forEach(opt => {
        if (opt.loader === 'babel-loader') {
            opt.options = {
                plugins: ['istanbul']
            };
        }
    });
});

const options = {
    webpackOptions,
    watchOptions: {},
};

module.exports = (on, config) => {
    on('file:preprocessor', webpack(options));

    return Object.assign({}, config, {
        integrationFolder: 'src',
        // screenshotsFolder: 'cypress/screenshots',
        // videosFolder: 'cypress/videos',
        // supportFile: 'cypress/support/index.js'
      })
};
// webpack4 版本

const webpack = require('@cypress/webpack-preprocessor');
const config = require('../../webpack.base');
config.mode = 'development';
config.module.rules[0].use.options = {
    plugins: ['istanbul']
};

module.exports = (on) => {
    const options = {
    // send in the options from your webpack.config.js, so it works the same
    // as your app's code
        webpackOptions: config,
        watchOptions: {},
    };

    on('file:preprocessor', webpack(options));
};
           

5、修改cypress/support

// support/index.js

import './commands';
import './istanbul';
           

在support目錄裡添加istanbul.js檔案

// https://github.com/cypress-io/cypress/issues/346#issuecomment-365220178
// https://github.com/cypress-io/cypress/issues/346#issuecomment-368832585
/* eslint-disable */
const istanbul = require('istanbul-lib-coverage');

const map = istanbul.createCoverageMap({});
const coverageFolder = Cypress.config('coverageFolder');
const coverageFile = `${ coverageFolder }/out-${Date.now()}.json`;

Cypress.on('window:before:unload', e => {
    const coverage = e.currentTarget.__coverage__;

    if (coverage) {
        map.merge(coverage);
    }
});

after(() => {
    cy.window().then(win => {
        const specWin = win.parent.document.querySelector('iframe[id~="Spec:"]').contentWindow;
        const unitCoverage = specWin.__coverage__;
        const coverage = win.__coverage__;

        if (unitCoverage) {
            map.merge(unitCoverage);
        }

        if (coverage) {
            map.merge(coverage);
        }

        cy.writeFile(coverageFile, JSON.stringify(map));
        cy.exec('npx nyc report --reporter=html -t=coverage')
        cy.exec('npm run coverage')
            .then(coverage => {
                // output coverage report
                const out = coverage.stdout
                    // 替換bash紅色辨別符
                    .replace(/[31;1m/g, '')
                    .replace(/[0m/g, '')
                    // 替換粗體辨別符
                    .replace(/[3[23];1m/g, '');
                console.log(out);
            })
            .then(() => {
                // output html file link to current test report
                const link = Cypress.spec.absolute
                    .replace(Cypress.spec.relative, `${coverageFolder}/${Cypress.spec.relative}`)
                    .replace('cypress.spec.', '');
                console.log(`check coverage detail: file://${link}.html`);
            });
    });
});
           

6、修改package.json (推薦使用git push hooks 裡跑test)

"gitHooks": {
        "pre-push": "npm run test"
    },
    "nyc": {
        "exclude": [
            "**/*.spec.js",
            "cypress",
            "example"
        ]
    }
           
note: 如果項目使用了sass來寫css,則必須指定node版本為v8.x.x,這個算是cypress的bug。

Issuess

# npm install n -g
# sudo n v8.9.0
# npm rebuild node-sass
           

這樣在git push之前會先跑單元測試,通過了才可以push成功。

vue init webpack缺少辨別符_Vue 之五 —— 單元測試

三、使用方法

  • 對于各個 utils 内的方法以及 vue元件,隻需在其目錄下補充同名的 xxx.spec.js,即可為其添加單元測試用例。
  • 斷言文法采用 cypress 斷言: https://docs.cypress.io/guides/references/assertions.html#Chai
  • vue元件測試使用官方推薦的test-utils: https://vue-test-utils.vuejs.org/
  • npm 指令測試:
  • npm run cy:run (終端測試,前置條件:必須啟動本地服務)
  • npm run cy:open (GUI 測試,前置條件:必須啟動本地服務)
  • npm run cy:dev (GUI測試, 自動啟動本地服務,成功後打開GUI)
  • npm run test (終端測試, 自動啟動本地服務,并且統計覆寫率,在終端運作,也是CI運作的測試指令)

四、測試原則

1、明白要測試的是什麼

不推薦一味追求行級覆寫率,因為它會導緻我們過分關注元件的内部實作細節,而隻

關注其輸入和輸出

。一個簡單的測試用例将會斷言一些輸入 (使用者的互動或 prop 的改變) 提供給某元件之後是否導緻預期結果 (渲染結果或觸發自定義事件)。

2、測試公共接口
vue init webpack缺少辨別符_Vue 之五 —— 單元測試
a、如果模闆有邏輯,我們應該測試它
// template
<button ref="logOutButton" v-if="loggedIn">Log out</button>
// Button.spec.js

const PropsData = {
    loggedIn: true,
};

it('hides the logOut button if user is not logged in', () => {
    const wrapper = mount(UserSettingsBar, { PropsData });
    const { vm } = wrapper;
    expect(vm.$refs.logOutButton).to.exist();
    wrapper.setProps({ loggedIn: false });
    expect(vm.$refs.logOutButton).not.to.exist();
});
           
原則:Props in Rendered Output
vue init webpack缺少辨別符_Vue 之五 —— 單元測試
b、什麼超出了我們元件的範圍
  • 實作細節,過分關注元件的内部實作細節,進而導緻瑣碎的測試。
  • 測試架構本身, 這是vue應該去做的事情。
1、<p> {{ myProp }} </p>
expect(p.text()).to.be(/ prop value /);

2、prop 校驗   
           
c、權衡
vue init webpack缺少辨別符_Vue 之五 —— 單元測試
Integration Test
vue init webpack缺少辨別符_Vue 之五 —— 單元測試
// Count.spec.js

it('should display the updated count after button is clicked', () => {
    const wrapper = mount(Count, { 
        count: 0
    });

    const ButtonInstance = wrapper.find(Button);
    const buttonEl = ButtonInstance.find('button')[0]; // find button click
    buttonE1.trigger('click');

    const CounterDisplayInstance = wrapper.find(CounterDisplay);
    const displayE1 = CounterDisplayInstance.find('.count-display')[0];
    expect(displayE1.text()).to.equal('1'); // find display, assert render
});
           
Shallow Test
vue init webpack缺少辨別符_Vue 之五 —— 單元測試
// Count.spec.js

it('should pass the "count" prop to CounterDisplay', () => {
    const counterWrapper = shallow(Counter, {
        count: 10
    });
    const counterDisplayWrapper = counterWrapper.find(CounterDisplay);
    // we dont't care how this will be rendered
    expect(counterDisplayWrapper.propsData().count).to.equal(10);
});

it('should update the "count" prop by 1 on Button "increment" event', () => {
    const counterWrapper = shallow(Counter, {
        count: 10
    });
    const buttonWrapper = counterWrapper.find(Button);
    // we don't care how this was triggered
    buttonWrapper.vm.$emit('increment');
    expect(CounterDisplay.propsData().count).to.equal(11);
});
           
vue init webpack缺少辨別符_Vue 之五 —— 單元測試

參考:

cypress-vue-unit-test

Vue Test Utils

Component Tests with Vue.js

繼續閱讀