從零開始做Vue前端架構(6)單元測試 & 程式碼覆蓋率

CodeLittlePrince發表於2018-04-23

新的一年(噗,都快年中了)

之前因為上家公司的經營出了問題,年前的大裁員,過了一個漫長的春節。 之後加入了新公司,然後正好趕上一個很緊急的專案,忙成狗,因此好久沒更新文章了。 不過,我又回來啦!

原文

從零開始做Vue前端架構(6)單元測試 & 程式碼覆蓋率

前言

自動化測試,我們將使用karma和nightmare,內容會包括:

  1. 單元測試
  2. e2e測試(放下一篇文章) 其實,單元測試一般用在寫公共包的時候,比如通用的js函式庫,通用的UI元件庫。基本不太會在做業務專案的時候還使用單元測試。 然後,e2e測試的話,那其實往往是測試工程師需要做的,往往使用selenium。 那難道前端就不需要學測試了嗎? 答案肯定是否定的,不然我寫個毛...... vue的專案就用了單元測試和e2e。 基於vue的UI元件庫,比如:餓了麼的element、滴滴的cube-ui、有讚的vant等都有單元測試(咩有e2e,因為沒必要)。 滴滴的話,我問了下黃軼大佬,他們專案前端自動化測試是用了單元測試和e2e的。 總之,兩種都是由應用場景的,e2e雖然用的不多,或者說有時候不是前端自動化的範疇,但是其實做起來很簡單,學會準沒錯!

一、單元測試

安裝karma

npm install karma --save-dev
npm install karma-jasmine karma-chrome-launcher jasmine-core --save-dev
複製程式碼

package.jsonscripts配置 "test": "karma start"

初始化karma

$ karma init my.conf.js

Which testing framework do you want to use?
Press tab to list possible options. Enter to move to the next question.
> mocha

Do you want to use Require.js?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture a browser automatically?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
>

What is the location of your source and test files?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Press Enter to move to the next question.
> test/**/*.js
>

Should any of the files included by the previous patterns be excluded?
You can use glob patterns, eg. "**/*.swp".
Press Enter to move to the next question.
>

Do you want Karma to watch all the files and run the tests on change?
Press tab to list possible options.
> yes
複製程式碼

初始成功以後,會生成一份karma.conf.js配置檔案。

順便,在根目錄建立一個test的資料夾,在這資料夾中建立一份index.js,內容為:

describe('A spec suite', function() {
  it('contains a passing spec', function() {
    console.log('Hello Karma')
  })
})
複製程式碼

執行一下: npm run test 我們會看到程式會自動開啟chrome瀏覽器,然後顯示測試結果。

正式的寫個單元測試

重新組織test資料夾

.
└── unit
    ├── index.js
    ├── karma.conf.js
    └── specs
        └── dom.spec.js
複製程式碼

因為我們還要做e2e測試,所以,在test下,用各個資料夾區分,unit下就是單元測試的內容了。

安裝一系列包

karma-webpack
karma-sourcemap-loader
karma-coverage
chai
sinon
sinon-chai
karma-sinon-chai
karma-mocha-reporter
複製程式碼

karma-webpack:因為我們的專案程式碼是用es6或者es7寫的,所以webpack編譯是必須的 karma-sourcemap-loader:sourcemap明顯是必要的 karma-coverage:做程式碼覆蓋率的時候需要用 chai:搭配mocha斷言 sinon:搭配mocha做spy、stub、mock sinon-chai:用chai來做sinon的斷言,可以說是擴充套件 karma-sinon-chai:方便在測試程式碼中的呼叫,直接就能用expect、sinon.spy等,不需要每個檔案都import karma-mocha-reporter:mocha測試完的報告

像karma、mocha、chai、sinon這種測試工具,我不會很詳細的介紹,全部都介紹的話內容實在有點多,而且也比較枯燥。想要學習可以看我的參考資料,是我花了大把時間篩選整理出來的。

修改karma.conf.js

const webpackConfig = require('../../webpack.config.test.js')

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['mocha', 'sinon-chai'],

    // list of files / patterns to load in the browser
    files: [
      './index.js'
    ],

    // list of files / patterns to exclude
    exclude: [
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      './index.js': ['webpack', 'sourcemap', 'coverage']
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['mocha', 'coverage'],

    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    },
    .
    .
    .
    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,

    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity,

    webpack: webpackConfig,
    webpackMiddleware: {
      stats: 'errors-only'
    }
  })
}
複製程式碼

karma原本在根目錄,我們直接移過來就好了。然後修改的不多,我稍微解釋一下:

  1. files:將要被測試的檔案
  2. preprocessors:在引入檔案前,需要用什麼方式處理,我們看到了,包括webpack、sourcemap、coverage
  3. reporters:測試完成後的報告,我們需要mocha的報告和coverage的報告
  4. coverageReporter:程式碼覆蓋率生成的報告檔案地址和存在形式設定
  5. webpack:在這需要引入webpack的配置,我們見到頂部,引入了webpack.test.config.js檔案,我們待會兒會介紹裡面的配置
  6. webpackMiddleware:stats: 'errors-only'我們讓webpack的編譯過程不顯示出來,除非編譯報錯

配置webpack.test.config.js

const webpackConfigBase = require('./webpack.config.base.js')

const config = Object.assign(webpackConfigBase.config, {
  // sourcemap 模式
  devtool: '#inline-source-map',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
})

module.exports = config
``

#### 編輯index.js入口檔案
這個檔案是為了配合`karma-webpack`的,詳情見[Alternative Usage](https://github.com/webpack-contrib/karma-webpack#alternative-usage)
```js
// require all test files (files that ends with .spec.js)
// 語法說明:https://doc.webpack-china.org/guides/dependency-management/#require-context
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

// require all src files which in the app/common/js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
const srcContext = require.context('../../app/common/js/', true, /\.js$/)
srcContext.keys().forEach(srcContext)
複製程式碼

測試程式碼放在./specs資料夾下,被測試原檔案在../../app/common/js/下。這裡我只測試一些公共的js檔案,如果你需要測試其它,可自行修改。比如一些基於vue的UI元件庫,你想要測試所有元件程式碼,還需要做些配置上的修改,這方面可以參考滴滴的cube-ui專案,挺完整的,覆蓋率也很高。

正式寫測試程式碼

編輯dom.spec.js檔案:

/**
 * 測試common/utils/dom.js
 */
import * as dom from 'common/js/utils/dom.js'

// const expect = require('chai').expect 裝過sinon-chai就不需要這句了;sinon同理

describe('utils/dom', () => {
  // 測試hasClass
  it('hasClass', () => {
    const ele = document.createElement('div')
    ele.className = 'base kitty'
    // 是否含有base
    expect(dom.hasClass(ele, 'base')).to.be.equal(true)
    // 是否含有kitty
    expect(dom.hasClass(ele, 'kitty')).to.be.equal(true)
    // 是否含有tom
    expect(dom.hasClass(ele, 'tom')).to.be.equal(false)
    // 無引數
    expect(dom.hasClass()).to.be.equal(false)
  })
  // 測試addClass
  it('addClass', () => {
    const ele = document.createElement('div')
    ele.className = 'base'
    // 增加類名kitty
    dom.addClass(ele, 'kitty')
    expect(ele.className).to.be.equal('base kitty')
    // 再增加類名kitty,希望並不會有重複類名
    dom.addClass(ele, 'kitty')
    expect(ele.className).to.be.equal('base kitty')
  })
  // 測試removeClass
  it('removeClass', () => {
    const ele = document.createElement('div')
    ele.className = 'base kitty'
    // 刪除類名kitty
    dom.removeClass(ele, 'kitty')
    expect(ele.className).to.be.equal('base')
    // 刪除不存在的類名
    dom.removeClass(ele, 'tom')
    expect(ele.className).to.be.equal('base')
  })
  // 測試noce
  it('once', () => {
    const ele = document.createElement('div')
    const callback = sinon.spy()
    dom.once(ele, 'click', callback)
    // 點選一次
    ele.click()
    expect(callback).to.have.been.calledOnce
    // 再點選一次,預期應該是不呼叫callback的,所以依然為calledOnce
    ele.click()
    expect(callback).to.have.been.calledOnce
  })
})
複製程式碼

程式碼註釋已經很清楚啦~

執行測試

先修改下package.json配置:"test:unit": "karma start test/unit/karma.conf.js" 執行:

➜  construct git:(master) npm run test:unit

> vue-construct@1.0.0 test:unit /Users/Terry/WFE/vue-study/construct
> karma start test/unit/karma.conf.js


START:
ℹ 「wdm」:
ℹ 「wdm」: Compiled successfully.
ℹ 「wdm」: Compiling...
ℹ 「wdm」:
ℹ 「wdm」: Compiled successfully.
23 04 2018 01:25:39.438:INFO [karma]: Karma v2.0.0 server started at http://0.0.0.0:9876/
23 04 2018 01:25:39.440:INFO [launcher]: Launching browser Chrome with unlimited concurrency
23 04 2018 01:25:39.448:INFO [launcher]: Starting browser Chrome
23 04 2018 01:25:41.778:INFO [Chrome 66.0.3359 (Mac OS X 10.13.2)]: Connected on socket A9ZeKTNtnUU9MAceAAAA with id 51610088
  utils/dom
    ✔ hasClass
    ✔ addClass
    ✔ removeClass
    ✔ once

Finished in 0.008 secs / 0.004 secs @ 01:25:41 GMT+0800 (CST)

SUMMARY:
✔ 4 tests completed

=============================== Coverage summary ===============================
Statements   : 87.12% ( 142/163 ), 14 ignored
Branches     : 61.25% ( 49/80 ), 22 ignored
Functions    : 86.11% ( 31/36 ), 5 ignored
Lines        : 90.79% ( 138/152 )
================================================================================
複製程式碼

參考資料

karma 測試框架的前世今生

karma thesis

karma 官網

前端自動化測試工具overview

前端自動化測試解決方案探析

mocha官網

程式碼測試覆蓋率分析

聊一聊前端自動化測試

Sinon指南: 使用Mocks, Spies 和 Stubs編寫JavaScript測試

sinon-chai github

論文是個很有意思的東西,看多了你會驚人地發現,很多大廠有深度的文章其實都是對論文的純翻譯~ 另外還參考了vue和滴滴的cube-ui的專案測試部分。

專案完整程式碼

Vue前端架構-by 子咻

相關文章