一步一步實現現代前端單元測試

xlaoyu發表於2018-01-18

2年前寫過一篇文章用Karma和QUnit做前端自動單元測試,只是大概講解了 karma 如何使用,針對的測試情況是傳統的頁面模式。本文中題目中【現代】兩字表明瞭這篇文章對比之前的最大不同,最近幾年隨著SPA(Single Page Application) 技術和各種元件化解決方案(Vue/React/Angular)的普及,我們開發的應用的組織方式和複雜度已經發生了翻天覆地的變化,對應我們的測試方式也必須跟上時代的發展。現在我們一步一步把各種不同的技術結合一起來完成頁面的單元測試和 e2e 測試。

1 karma + mocha + power assert

  • karma - 是一款測試流程管理工具,包含在執行測試前進行一些動作,自動在指定的環境(可以是真實瀏覽器,也可以是 PhantamJS 等 headless browser)下執行測試程式碼等功能。
  • mocha - 測試框架,類似的有 jasminejest 等。個人感覺 mocha 對非同步的支援和反饋資訊的顯示都非常不錯。
  • power assert - 斷言庫,特點是 No API is the best API。錯誤顯示異常清晰,自帶完整的自描述性。
    1) Array #indexOf() should return index when the value is present:
       AssertionError: # path/to/test/mocha_node.js:10
    
    assert(ary.indexOf(zero) === two)
           |   |       |     |   |
           |   |       |     |   2
           |   -1      0     false
           [1,2,3]
    
    [number] two
    => 2
    [number] ary.indexOf(zero)
    => -1
    複製程式碼

以下所有命令假設在 test-demo 專案下進行操作。

1.1 安裝依賴及初始化

# 為了操作方便在全域性安裝命令列支援
~/test-demo $ npm install karma-cli -g

# 安裝 karma 包以及其他需要的外掛和庫,這裡不一一闡述每個庫的作用
~/test-demo $ npm install karma mocha power-assert karma-chrome-launcher karma-mocha karma-power-assert karma-spec-reporter karma-espower-preprocessor cross-env -D

# 建立測試目錄
~/test-demo $ mkdir test

# 初始化 karma
~/test-demo $ karma init ./test/karma.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 any browsers 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".
Enter empty string to move to the next question.
>

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string 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.
> no
複製程式碼

生成的配置檔案略作修改,如下(因篇幅原因,隱藏了註釋):

module.exports = function(config) {
  config.set({
    basePath: '',

    // 表示可以在測試檔案中不需引入即可使用兩個庫的全域性方法
    frameworks: ['mocha', 'power-assert'],
    files: [
      '../src/utils.js',
      './specs/utils.spec.js.js'
    ],
    exclude: [
    ],
    preprocessors: {
      './specs/utils.spec.js': ['espower']
    },
    reporters: ['spec'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: false,
    browsers: ['Chrome'],
    singleRun: false,
    concurrency: Infinity
  })
}
複製程式碼

1.2 待測試程式碼

我們把原始檔放在src目錄下。

// src/utils.js
function reverseString(string) {
  return string.split('').reverse().join('');
}
複製程式碼

1.3 測試程式碼

測試程式碼放在test/specs目錄下,每個測試檔案以 .spec.js 作為字尾。

// test/spes/utils.spec.js
describe('first test', function() {
  it('test string reverse => true', function() {
    assert(reverseString('abc') === 'cba');
  });

  it('test string reverse => false', function() {
    assert(reverseString('abc') === 'cba1');
  });
});
複製程式碼

1.4 執行測試

回到專案根目錄,執行命令 npm run test 開始執行測試,然後看到瀏覽器會自動開啟執行測試,命令列輸出結果如下:

[karma]: Karma v2.0.0 server started at http://0.0.0.0:9876/
[launcher]: Launching browser Chrome with unlimited concurrency
[launcher]: Starting browser Chrome
[Chrome 63.0.3239 (Mac OS X 10.13.1)]: Connected on socket HEw50fXV-d24BZGBAAAA with id 24095855

  first testtest string reverse => truetest string reverse => false
	AssertionError:   # utils.spec.js:9

	  assert(reverseString('abc') === 'cba1')
	         |                    |
	         "cba"                false

	  --- [string] 'cba1'
	  +++ [string] reverseString('abc')
	  @@ -1,4 +1,3 @@
	   cba
	  -1

Chrome 63.0.3239 (Mac OS X 10.13.1): Executed 2 of 2 (1 FAILED) (0.022 secs / 0.014 secs)
TOTAL: 1 FAILED, 1 SUCCESS
複製程式碼

可以看出一個測試成功一個測試失敗。

2 測試覆蓋率(test coverage)

測試覆蓋率是衡量測試質量的主要標準之一,含義是當前測試對於原始碼的執行覆蓋程度。在 karma 中使用 karma-coverage 外掛即可輸出測試覆蓋率,外掛底層使用的是 istanbul

~/test-demo $ npm i karma-coverage -D
複製程式碼

修改 karma.conf.js 檔案:

preprocessors: {
  '../src/utils.js': ['coverage'],
  './specs/utils.spec.js': ['espower']
},

reporters: ['spec', 'coverage'],
coverageReporter: {
  dir: './coverage', // 覆蓋率結果檔案放在 test/coverage 資料夾中
  reporters: [
    { type: 'lcov', subdir: '.' },
    { type: 'text-summary' }
  ]
},
複製程式碼

再次執行測試命令,在最後會輸出測試覆蓋率資訊

=============================== Coverage summary ===============================
Statements   : 100% ( 2/2 )
Branches     : 100% ( 0/0 )
Functions    : 100% ( 1/1 )
Lines        : 100% ( 2/2 )
================================================================================
複製程式碼

開啟 test/coverage/lcov-report/index.html 網頁可以看到詳細資料

coverage.gif

3 webpack + babel

上面的例子,只能用於測試使用傳統方式編寫的 js 檔案。為了模組化元件化,我們可能會使用ES6commonjsAMD等模組化方案,然後使用 webpack 的 umd 打包方式輸出模組以相容不同的使用方式。一般我們還需要使用ES6+的新語法,需要在 webpack 中加入babel作為轉譯外掛。

webpack 和 babel 的使用以及需要的依賴和配置,這裡不做詳細說明,因為主要是按照專案需要走,本文僅指出為了測試而需要修改的地方

3.1 安裝依賴

~/test-demo $ npm i babel-plugin-istanbul babel-preset-power-assert karma-sourcemap-loader karma-webpack -D
複製程式碼

3.2 修改配置

.babelrc

power-assert以及coverage的程式碼注入修改為在babel編譯階段進行,在.babelrc 檔案中加入以下配置:

{
  "env": {
    "test": {
      "presets": ["env", "babel-preset-power-assert"],
      "plugins": ["istanbul"]
    }
  }
}
複製程式碼

test/index.js

在測試檔案以及原始碼檔案都非常多的情況下,或者我們想讓我們的測試程式碼也使用上ES6+的語法和功能,我們可以建立一個入口來統一引入這些檔案,然後使用 webpack 處理整個入口,在test目錄下新建index.js

// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

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

karma.conf.js 修改已經增加對應的配置

{
  files: [
    './index.js'
  ],
  preprocessors: {
    './index.js': ['webpack', 'sourcemap'],
  },
  webpack: webpackConfig,
  webpackMiddleware: {
    noInfo: false
  },
}
複製程式碼

utils.spec.js

import reverseString from '../../src/utils';

describe('first test', function() {
  it('test string reverse => true', function() {
    assert(reverseString('abc') === 'cba');
  });

  it('test string reverse => false', function() {
    assert(reverseString('abc') === 'cba1');
  });
});
複製程式碼

3.3 執行測試

執行測試,能得到和第二步相同的結果。

4 vue

如果專案中使用了 vue,我們想對封裝的元件進行測試,也非常簡單。

首先 webpack 配置中新增處理 vue 的邏輯,安裝需要的依賴,這裡不再贅述。

src目錄下新增HelloWorld.vue

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <h2>Essential Links</h2>
    
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>
複製程式碼

然後新增測試程式碼:

// test/specs/vue.spec.js
import Vue from 'vue';
import HelloWorld from '@/HelloWorld';

describe('HelloWorld.vue', () => {
  it('should render correct contents', () => {
    const Constructor = Vue.extend(HelloWorld)
    const vm = new Constructor().$mount()
    assert(vm.$el.querySelector('.hello h1').textContent === 'Welcome to Your Vue.js App')
  })

})
複製程式碼

執行測試,可以看到命令列輸出:

 first testtest string reverse => truetest string reverse => false
        AssertionError:   # test/specs/utils.spec.js:9

          assert(reverseString('abc') === 'cba1')
                 |                    |
                 "cba"                false

          --- [string] 'cba1'
          +++ [string] reverseString('abc')
          @@ -1,4 +1,3 @@
           cba
          -1

  HelloWorld.vue
    ✓ should render correct contents
複製程式碼

這裡 Vue 能替換為其他任意的前端框架,只需要按照對應框架的配置能正確打包即可。

結語

上面所有程式碼都放在了這個專案,可以把專案下載下來手動執行檢視結果。

以上大概講解了現代前端測試的方法和過程,但是有人會問,我們為什麼需要搞那麼多事情,寫那麼多程式碼甚至測試程式碼比真實程式碼還要多呢?這裡引用 Egg 官方一段話回答這個問題:

先問我們自己以下幾個問題:
  - 你的程式碼質量如何度量?  
  - 你是如何保證程式碼質量?  
  - 你敢隨時重構程式碼嗎?  
  - 你是如何確保重構的程式碼依然保持正確性?  
  - 你是否有足夠信心在沒有測試的情況下隨時釋出你的程式碼?  

如果答案都比較猶豫,那麼就證明我們非常需要單元測試。  
它能帶給我們很多保障:  
  - 程式碼質量持續有保障  
  - 重構正確性保障  
  - 增強自信心  
  - 自動化執行   

Web 應用中的單元測試更加重要,在 Web 產品快速迭代的時期,每個測試用例都給應用的穩定性提供了一層保障。 API 升級,測試用例可以很好地檢查程式碼是否向下相容。 對於各種可能的輸入,一旦測試覆蓋,都能明確它的輸出。 程式碼改動後,可以通過測試結果判斷程式碼的改動是否影響已確定的結果。
複製程式碼

是不是消除了很多心中的疑惑?

以上內容如有錯漏,或者有其他看法,請留言共同探討。


版權宣告:原創文章,歡迎轉載,交流 ,請註明出處“本文發表於xlaoyu.info“。

相關文章