本文做者: 江水javascript
本文主要介紹前端單元測試的一些技術方案。css
單元測試的技術方案不少,不一樣工具之間有互相協同,也存在功能重合,給咱們搭配測試方案帶來不小的困難,並且隨着 ES6
, TypeScript
的出現,單元測試又增長了不少其餘步驟,完整配置起來每每須要很大的時間成本。我但願經過對這些工具的各自做用的掌握,瞭解完整的前端測試技術方案。前端單元測試的領域也不少,這裏主要講對於前端組件如何進行單元測試,最後會主要介紹下對於 React
組件的一些測試方法總結。html
單元測試最核心的部分就是作斷言,好比傳統語言中的 assert
函數,若是當前程序的某種狀態符合 assert
的指望此程序才能正常執行,不然直接退出應用。因此咱們能夠直接用 Node
中自帶的 assert
模塊作斷言。前端
用最簡單的例子作個驗證java
function multiple(a, b) {
let result = 0;
for (let i = 0; i < b; ++i)
result += a;
return result;
}
複製代碼
const assert = require('assert');
assert.equal(multiple(1, 2), 3));
複製代碼
這種例子可以知足基礎場景的使用,也能夠做爲一種單元測試的方法。node
nodejs
自帶的 assert
模塊提供了下面一些斷言方法,只能知足一些簡單場景的須要。react
assert.deepEqual(actual, expected[, message])
assert.deepStrictEqual(actual, expected[, message])
assert.doesNotMatch(string, regexp[, message])
assert.doesNotReject(asyncFn[, error][, message])
assert.doesNotThrow(fn[, error][, message])
assert.equal(actual, expected[, message])
assert.fail([message])
assert.ifError(value)
assert.match(string, regexp[, message])
assert.notDeepEqual(actual, expected[, message])
assert.notDeepStrictEqual(actual, expected[, message])
assert.notEqual(actual, expected[, message])
assert.notStrictEqual(actual, expected[, message])
assert.ok(value[, message])
assert.rejects(asyncFn[, error][, message])
assert.strictEqual(actual, expected[, message])
assert.throws(fn[, error][, message])
複製代碼
自帶的 assert
不是專門給單元測試使用, 提供的錯誤信息文檔性很差,上面的 demo
最終執行下來會產生下面的報告:webpack
$ node index.js
assert.js:84
throw new AssertionError(obj);
^
AssertionError [ERR_ASSERTION]: 2 == 3
at Object.<anonymous> (/home/quanwei/git/index.js:4:8)
at Module._compile (internal/modules/cjs/loader.js:778:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
at startup (internal/bootstrap/node.js:283:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)
複製代碼
因爲自帶的模塊依賴 Node
自身的版本,沒辦法自由升級,因此使用內置的包靈活性有時候不太夠,另外咱們不少斷言函數也須要在瀏覽器端執行,因此咱們須要同時支持瀏覽器和 Node
端的斷言庫。同時觀察上面的輸出能夠發現,這個報告更像是程序的錯誤報告,而不是一個單元測試報告。而咱們在作單元測時每每須要斷言庫可以提供良好的測試報告,這樣才能一目瞭然地看到有哪些斷言經過沒經過,因此使用專業的單元測試斷言庫仍是頗有必要。git
chai
是目前很流行的斷言庫,相比於同類產品比較突出。chai
提供了 TDD (Test-driven development)和 BDD (Behavior-driven development) 兩種風格的斷言函數,這裏不會過多介紹兩種風格的優缺,本文主要以 BDD
風格作演示。es6
var assert = require('chai').assert
, foo = 'bar'
, beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
assert.typeOf(foo, 'string'); // without optional message
assert.typeOf(foo, 'number', 'foo is a number'); // with optional message
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');
複製代碼
chai
比 Node
自帶的 assert
增長了一個斷言說明參數,能夠經過這個參數提升測試報告的可讀性
$ node chai-assert.js
/home/quanwei/git/learn-tdd-bdd/node_modules/chai/lib/chai/assertion.js:141
throw new AssertionError(msg, {
^
AssertionError: foo is a number: expected 'bar' to be a number
at Object.<anonymous> (/home/quanwei/git/learn-tdd-bdd/chai-assert.js:6:8)
at Module._compile (internal/modules/cjs/loader.js:778:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
at Module.load (internal/modules/cjs/loader.js:653:32)
at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
at Function.Module._load (internal/modules/cjs/loader.js:585:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)
at startup (internal/bootstrap/node.js:283:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:623:3)
複製代碼
chai
的 BDD
風格使用 expect
函數做爲語義的起始,也是目前幾乎全部 BDD
工具庫都遵循的風格。
chai
的 expect
斷言風格以下
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
複製代碼
BDD
的思想就是寫單元測試就像寫產品需求,而不關心內部邏輯,每個用例閱讀起來就像一篇文檔。例以下面的用例:
expect(foo).to.be.a('string')
expect(foo).to.include('bar')
expect(foo).to.not.include('biz')
能夠看到這種風格的測試用例可讀性更強。
其餘的斷言庫還有 expect.js
should.js
better-assert , unexpected.js 這些斷言庫都只提供純粹的斷言函數,能夠根據喜愛選擇不一樣的庫使用。
有了斷言庫以後咱們還須要使用測試框架將咱們的斷言更好地組織起來。
mocha
是一個經典的測試框架(Test Framework),測試框架提供了一個單元測試的骨架,能夠將不一樣子功能分紅多個文件,也能夠對一個子模塊的不一樣子功能再進行不一樣的功能測試,從而生成一份結構型的測試報告。例如 mocha
就提供了describe
和 it
描述用例結構,提供了 before
, after
, beforeEach
, afterEach
生命週期函數,提供了 describe.only
,describe.skip
, it.only
, it.skip
用以執行指定部分測試集。
const { expect } = require('chai');
const { multiple } = require('./index');
describe('Multiple', () => {
it ('should be a function', () => {
expect(multiple).to.be.a('function');
})
it ('expect 2 * 3 = 6', () => {
expect(multiple(2, 3)).to.be.equal(6);
})
})
複製代碼
測試框架不依賴底層的斷言庫,哪怕使用原生的 assert
模塊也能夠進行。給每個文件都要手動引入 chai
比較麻煩 ,這時候能夠給 mocha
配置全局腳本,在項目根目錄 .mocharc.js
文件中加載斷言庫, 這樣每一個文件就能夠直接使用 expect
函數了。
// .mocharc.js
global.expect = require('chai').expect;
複製代碼
使用 mocha 能夠將咱們的單元測試輸出成一份良好的測試報告 mocha *.test.js
當出現錯誤時輸出以下
由於運行在不一樣環境中須要的包格式不一樣,因此須要咱們針對不一樣環境作不一樣的包格式轉換,爲了瞭解在不一樣端跑單元測試須要作哪些事情,能夠先來了解一下常見的包格式。
目前咱們主流有三種模塊格式,分別是 AMD
, CommonJS
, ES Module
。
AMD 是 RequireJS
推廣過程當中流行的一個比較老的規範,目前不管瀏覽器仍是 Node
都沒有默認支持。AMD
的標準定義了 define
和 require
函數,define
用來定義模塊及其依賴關係,require
用以加載模塊。例如
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Document</title>
+ <script
+ src="https://requirejs.org/docs/release/2.3.6/minified/require.js"></script>
+ <script src="./index.js" />
</head>
<body></body>
</html>
複製代碼
// index.js
define('moduleA', ['https://some/of/cdn/path'], function() {
return { name: 'moduleA' };
});
define(function(require) {
const fs = require('fs');
return fs;
})
define('moduleB', function() {
return { name: 'module B' }
});
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
console.log(module);
});
複製代碼
這裏使用了RequireJS
做爲 AMD
引擎, 能夠看到 define
函數會定義當前依賴了哪些模塊並將模塊加載完成後異步回調給當前模塊,這種特性使得 AMD 尤其適合瀏覽器端異步加載。
咱們可使用 webpack
打包一份 amd
模塊看下真實代碼
// entry.js
export default function sayHello() {
return 'hello amd';
}
複製代碼
// webpack.config.js
module.exports = {
mode: 'development',
devtool: false,
entry: './entry.js',
output: {
libraryTarget: 'amd'
}
}
複製代碼
最終生成代碼(精簡了不相關的邏輯)
// dist/main.js
define(() => ({
default: function sayHello() {
return 'hello amd';
}
}));
複製代碼
在瀏覽器/Node
中想要使用 AMD
須要全局引入 RequireJS
,對單元測試而言比較典型的問題是在初始化 karma
時會詢問是否使用 RequireJS
,不過通常如今不多有人使用了。
能夠縮寫成CJS
, 其 規範 主要是爲了定義 Node
的包格式,CJS
定義了三個關鍵字, 分別爲 require
,exports
, module
, 目前幾乎全部Node
包以及前端相關的NPM
包都會轉換成該格式, CJS
在瀏覽器端須要使用 webpack
或者 browserify
等工具打包後才能執行。
ES Module
是 ES 2015
中定義的一種模塊規範,該規範定義了 表明爲 import
和 export
,是咱們開發中經常使用的一種格式。雖然目前不少新版瀏覽器都支持<script type="module">
了,支持在瀏覽器中直接運行 ES6
代碼,可是瀏覽器不支持 node_modules
,因此咱們的原始 ES6
代碼在瀏覽器上依然沒法運行,因此這裏我暫且認爲瀏覽器不支持 ES6
代碼, 依然須要作一次轉換。
下表爲每種格式的支持範圍,括號內表示須要藉助外部工具支持。
Node | 瀏覽器 | |
---|---|---|
AMD | 不支持(require.js, r.js) | 不支持(require.js) |
CommonJS | 支持 | 不支持(webpack/browserify) |
ESModule | 不支持(babel) | 不支持(webpack) |
單元測試要在不一樣的環境下執行就要打不一樣環境對應的包,因此在搭建測試工具鏈時要肯定本身運行在什麼環境中,若是在 Node
中只須要加一層 babel
轉換,若是是在真實瀏覽器中,則須要增長 webpack
處理步驟。
因此爲了可以在 Node
環境的 Mocha
中使用 ES Module
有兩種方式
Node
環境天生支持 ES Module
(node version >= 15)babel
代碼進行一次轉換第一種方式略過,第二種方式使用下面的配置
npm install @babel/register @babel/core @babel/preset-env --save-dev
複製代碼
// .mocharc.js
+ require('@babel/register');
global.expect = require('chai').expect;
複製代碼
// .babelrc
+ {
+ "presets": ["@babel/preset-env" ,「@babel/preset-typescript」]
+ }
複製代碼
一樣地若是在項目中用到了 TypeScript
, 就可使用ts-node/register
來解決,由於 TypeScript
自己支持 ES Module
轉換成 CJS
, 因此支持了 TypeScript
後就不須要使用 babel
來轉換了。(這裏假設使用了 TypeScript
的默認配置)
npm install ts-node typescript --save-dev
複製代碼
// .mocharc.js
require('ts-node/register');
複製代碼
Mocha
自身支持瀏覽器和 Node
端測試,爲了在瀏覽器端測試咱們須要寫一個 html, 裏面使用 <script src="mocha.min.js">
的文件,而後再將本地全部文件插入到html中才能完成測試,手動作工程化效率比較低,因此須要藉助工具來實現這個任務,這個工具就是 Karma
。
Karma
本質上就是在本地啓動一個web服務器,而後再啓動一個外部瀏覽器加載一個引導腳本,這個腳本將咱們全部的源文件和測試文件加載到瀏覽器中,最終就會在瀏覽器端執行咱們的測試用例代碼。因此使用 Karma
+ mocha
+chai
便可搭建一個完整的瀏覽器端的單元測試工具鏈。
npm install karma mocha chai karma-mocha karma-chai --save-dev
npx karma init
// Which testing framework do you want to use: mocha
// Do you want to use Require.js: no
// Do you want capture any browsers automatically: Chrome
複製代碼
這裏 Karma
初始化時選擇了 Mocha
的支持,而後第二個 Require.js
通常爲否,除非業務代碼中使用了amd
類型的包。第三個選用 Chrome
做爲測試瀏覽器。 而後再在代碼裏單獨配置下 chai
。
// karma.conf.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'],
+ frameworks: ['mocha', 'chai'],
// list of files / patterns to load in the browser
files: [],
複製代碼
Karma
的 frameworks
做用是在全局注入一些依賴,這裏的配置就是將 Mocha
和 chai
提供的測試相關工具暴露在全局上供代碼裏使用。 Karma
只是將咱們的文件發送到瀏覽器去執行,可是根據前文所述咱們的代碼須要通過 webpack
或 browserify
打包後才能運行在瀏覽器端。
若是原始代碼已是 CJS
了,可使用 browserify
來支持瀏覽器端運行,基本零配置,可是每每現實世界比較複雜,咱們有 ES6
,JSX
以及 TypeScript
要處理,因此這裏咱們使用 webpack
。
下面是 webpack
的配置信息。
npm install karma-webpack@4 webpack@4 @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev
複製代碼
// karma.conf.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', 'chai'],
// list of files / patterns to load in the browser
files: [
+ { pattern: "test/*.test.js", watched: false }
],
preprocessors: {
+ 'test/**/*.js': [ 'webpack']
},
+ webpack: {
+ module: {
+ rules: [{
+ test: /.*\.js/,
+ use: 'babel-loader'
+ }]
+ }
+ },
複製代碼
// .babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
複製代碼
這裏咱們測試一個React
程序代碼以下
// js/index.js
import React from 'react';
import ReactDOM from 'react-dom';
export function renderToPage(str) {
const container = document.createElement('div');
document.body.appendChild(container);
console.log('there is real browser');
return new Promise(resolve => {
ReactDOM.render(<div>{ str } </div>, container, resolve);
});
}
// test/index.test.js
import { renderToPage } from '../js/index';
describe('renderToPage', () => {
it ('should render to page', async function () {
let content = 'magic string';
await renderToPage(content);
expect(document.documentElement.innerText).to.be.contain(content);
})
})
複製代碼
而且打開了本地瀏覽器
能夠看到如今已經在真實瀏覽器中運行測試程序了。
由於圖形化的測試對 CI
機器不友好,因此能夠選擇 puppeteer
代替 Chrome
。
再者這些都是很重的包,若是對真實瀏覽器依賴性不強,可使用 JSDOM
在 Node
端模擬一個瀏覽器環境。
稍微總結下工具鏈
mocha
+ chai
+ babel
mocha
+ chai
+ babel
+ jsdom
karma
+ mocha
+ chai
+ webpack
+ babel
一個測試流水線每每須要不少個工具搭配使用,配置起來比較繁瑣,還有一些額外的工具例如單元覆蓋率(istanbul),函數/時間模擬 (sinon.js)等工具。工具之間的配合有時候不必定可以完美契合,選型費時費力。
jasmine
的出現就稍微緩解了一下這個問題,但也不夠完整,jasmine
提供一個測試框架,裏面包含了 測試流程框架,斷言函數,mock工具等測試中會遇到的工具。能夠近似地看做 jasmine = mocha + chai + 輔助工具
。
接下來試一試 jasmine
的工做流程。
使用 npx jasmine init
初始化以後會在當前目錄中生成spec
目錄, 其中包含一份默認的配置文件
// ./spec/support/jasmine.json
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": true
}
複製代碼
若是但願加載一些全局的配置能夠在 spec/helpers
目錄中放一些js
文件, 正如配置所言,jasmine 在啓動時會去執行 spec/helpers
目錄下的全部js
文件。
好比咱們經常使用 es6
語法,就須要增長es6
的支持。
新增 spec/helpers/babel.js
寫入以下配置便可。
npm install @babel/register @babel/core @babel/preset-env --save-dev
複製代碼
// spec/helpers/babel.js
require('babel-register');
複製代碼
// .babelrc
{
"presets": ["@babel/preset-env"]
}
複製代碼
和 mocha
同樣,若是須要 TypeScript
的支持,可使用以下配置
npm install ts-node typescript --save-dev
複製代碼
// spec/helpers/typescript.js
require('ts-node/register');
複製代碼
配置文件中的 spec_dir
是 jasmine
約定的用例文件目錄,spec_files
規定了用例文件格式爲 xxx.spec.js
。
有了這份默認配置就能夠按照要求寫用例,例如
// ./spec/index.spec.js
import { multiple } from '../index.js';
describe('Multiple', () => {
it ('should be a function', () => {
expect(multiple).toBeInstanceOf(Function);
})
it ('should 7 * 2 = 14', () => {
expect(multiple(7, 2)).toEqual(14);
})
it ('should 7 * -2 = -14', () => {
expect(multiple(7, -2)).toEqual(-14);
})
})
複製代碼
jasmine
的斷言風格和 chai
很不同,jasmine
的 API
以下,與 chai
相比少寫了不少 .
,並且支持的功能更加清晰,不用考慮如何組合使用的問題,並且下文介紹的 jest
測試框架也是使用這種風格。
nothing()
toBe(expected)
toBeCloseTo(expected, precisionopt)
toBeDefined()
toBeFalse()
toBeFalsy()
toBeGreaterThan(expected)
toBeGreaterThanOrEqual(expected)
toBeInstanceOf(expected)
toBeLessThan(expected)
toBeLessThanOrEqual(expected)
toBeNaN()
toBeNegativeInfinity()
toBeNull()
toBePositiveInfinity()
toBeTrue()
toBeTruthy()
toBeUndefined()
toContain(expected)
toEqual(expected)
toHaveBeenCalled()
toHaveBeenCalledBefore(expected)
toHaveBeenCalledOnceWith()
toHaveBeenCalledTimes(expected)
toHaveBeenCalledWith()
toHaveClass(expected)
toHaveSize(expected)
toMatch(expected)
toThrow(expectedopt)
toThrowError(expectedopt, messageopt)
toThrowMatching(predicate)
withContext(message) → {matchers}
複製代碼
運行 jasmine
便可生成測試報告
默認的測試報告不是很直觀, 若是但願提供相似 Mocha
風格的報告能夠安裝 jasmine-spec-reporter
,在 spec/helpers
目錄中添加一個配置文件, 例如spec/helpers/reporter.js
。
const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
jasmine.getEnv().clearReporters(); // remove default reporter logs
jasmine.getEnv().addReporter(new SpecReporter({ // add jasmine-spec-reporter
spec: {
displayPending: true
}
}));
複製代碼
此時輸出的用例報告以下
若是在 Jasmine
中執行 DOM 級別的測試,就依然須要藉助 Karma
或 JSDOM
了,具體的配置這裏就再也不贅述。
總結下 Jasmine
的工具鏈
Jasmine
+ babel
JSDOM
測試 : Jasmine
+ JSDOM
+ babel
Karma
+ Jasmine
+ webpack
+ babel
Jest
是 facebook
出的一個完整的單元測試技術方案,集 測試框架, 斷言庫, 啓動器, 快照,沙箱,mock工具於一身,也是 React
官方使用的測試工具。Jest
和 Jasmine
具備很是類似的 API
,因此在 Jasmine
中用到的工具在 Jest
中依然能夠很天然地使用。能夠近似看做 Jest = JSDOM 啓動器 + Jasmine
。
雖然 Jest 提供了很豐富的功能,可是並無內置 ES6
支持,因此依然須要根據不一樣運行時對代碼進行轉換,因爲 Jest 主要運行在 Node
中,因此須要使用 babel-jest
將 ES Module
轉換成 CommonJS
。
Jest 的默認配置
npm install jest --save-dev
npx jest --init
√ Would you like to use Jest when running "test" script in "package.json"? ... yes
√ Would you like to use Typescript for the configuration file? ... no
√ Choose the test environment that will be used for testing » jsdom (browser-like)
√ Do you want Jest to add coverage reports? ... no
√ Which provider should be used to instrument code for coverage? » babel
√ Automatically clear mock calls and instances between every test? ... yes
複製代碼
在 Node
或 JSDOM
下增長 ES6
代碼的支持
npm install jest-babel @babel/core @babel/preset-env
複製代碼
// .babelrc
{
"presets": ["@babel/preset-env"]
}
複製代碼
// jest.config.js
// 下面兩行爲默認配置,不寫也能夠
{
+ testEnvironment: "jsdom",
+ transform: {"\\.[jt]sx?$": "babel-jest"}
}
複製代碼
使用 Jest
生成測試報告
對於 React
和 TypeScript
支持也能夠經過修改 babel
的配置解決
npm install @babel/preset-react @babel/preset-typescript --save-dev
複製代碼
// .babrlrc
{
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}
複製代碼
目前 Jest
不支持直接在真實瀏覽器中進行測試,其默認的啓動器只提供了一個 JSDOM
環境,在瀏覽器中進行單元測試目前只有 Karma
方案能作到,因此也可使用 Karma
+ Jest
方案實現,可是不建議這麼作,由於 Jest
自身過重,使用 Karma
+ Jasmine
能達到基本同樣的效果。
另外還有一個比較流行的 E2E
方案 Jest
+ Puppeteer
, 因爲 E2E
不屬於單元測試範疇,這裏再也不展開。
Jest
工具鏈總結
Jest
+ babel
JSDOM
測試 : Jest
+ babel
E2E
測試 : Jest
+ Puppeteer
上面的內容介紹了 chai
, mocha
, karma
, jasmine
和 jest
, 每種工具分別對應一些本身特有的工具鏈,在選取合適的測試工具時根據實際須要選擇, 測試領域還有很是多的工具數都數不過來,下面來看下 React 單元測試的一些方法。
Enzyme
基礎配置以下:
npm install enzyme enzyme-adapter-react-16 jest-enzyme jest-environment-enzyme jest-canvas-mock react@16 react-dom@16 --save-dev
複製代碼
// jest.config.js
{
- "testEnvironment": "jsdom",
+ setupFilesAfterEnv: ["jest-enzyme", "jest-canvas-mock"],
+ testEnvironment: "enzyme",
+ testEnvironmentOptions: {
+ "enzymeAdapter": "react16"
+ },
}
複製代碼
jest-canvas-mock
這個包是爲了解決一些使用 JSDOM
未實現行爲觸發警告的問題。
上面創建了一個使用 Enzyme
比較友好的環境,能夠直接在全局做用域裏引用 React
, shallow
, mount
等 API
。此外 Enzyme
還註冊了許多友好的斷言函數到 Jest
中,以下所示,參考地址
toBeChecked()
toBeDisabled()
toBeEmptyRender()
toExist()
toContainMatchingElement()
toContainMatchingElements()
toContainExactlyOneMatchingElement()
toContainReact()
toHaveClassName()
toHaveDisplayName()
toHaveHTML()
toHaveProp()
toHaveRef()
toHaveState()
toHaveStyle()
toHaveTagName()
toHaveText()
toIncludeText()
toHaveValue()
toMatchElement()
toMatchSelector()
複製代碼
// js/ClassComponent.js
import React from 'react';
export default class ClassComponent extends React.PureComponent {
constructor() {
super();
this.state = { name: 'classcomponent' };
}
render() {
return (
<div> a simple class component <CustomComponent /> </div>
);
}
}
// test/hook.test.js
import HookComponent from '../js/HookComponent';
describe('HookComponent', () => {
it ('test with shallow', () => {
const wrapper = shallow(<HookComponent id={1} />);
expect(wrapper).toHaveState('name', 'classcomponent');
expect(wrapper).toIncludeText('a simple class component');
expect(wrapper).toContainReact(<div>a simple class component</div>);
expect(wrapper).toContainMatchingElement('CustomComponent');
})
})
複製代碼
Enzyme
提供了三種渲染組件方法
shallow
使用 react-test-renderer
將組件渲染成內存中的對象, 能夠方便進行 props
, state
等數據方面的測試,對應的操做對象爲 ShallowWrapper
,在這種模式下僅能感知到第一層自定義子組件,對於自定義子組件內部結構則沒法感知。mount
使用 react-dom
渲染組件,會建立真實 DOM
節點,比 shallow
相比增長了可使用原生 API
操做 DOM
的能力,對應的操做對象爲 ReactWrapper
,這種模式下感知到的是一個完整的 DOM
樹。render
使用 react-dom-server
渲染成 html
字符串,基於這份靜態文檔進行操做,對應的操做對象爲 CheerioWrapper
。由於 shallow
模式僅能感知到第一層自定義子組件組件,每每只能用於簡單組件測試。例以下面的組件
// js/avatar.js
function Image({ src }) {
return <img src={src} />;
}
function Living({ children }) {
return <div className="icon-living"> { children } </div>;
}
function Avatar({ user, onClick }) {
const { living, avatarUrl } = user;
return (
<div className="container" onClick={onClick}> <div className="wrapper"> <Living > <div className="text"> 直播中 </div> </Living> </div> <Image src={avatarUrl} /> </div>
)
}
export default Avatar;
複製代碼
shallow
渲染雖然不是真正的渲染,可是其組件生命週期會完整地走一遍。
使用 shallow(<Avatar />)
能感知到的結構以下, 注意看到 div.text
做爲 Living
組件的 children
可以被檢測到,可是 Living
的內部結構沒法感知。
Enzyme
支持的選擇器支持咱們熟悉的 css selector
語法,這種狀況下咱們能夠對 DOM
結構作以下測試
// test/avatar.test.js
import Avatar from '../js/avatar';
describe('Avatar', () => {
let wrapper = null, avatarUrl = 'abc';
beforeEach(() => {
wrapper = shallow(<Avatar user={{ avatarUrl: avatarUrl }} />);
})
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
})
it ('should render success', () => {
// wrapper 渲染不爲空
expect(wrapper).not.toBeEmptyRender();
// Image 組件渲染不爲空, 這裏會執行 Image 組件的渲染函數
expect(wrapper.find('Image')).not.toBeEmptyRender();
// 包含一個節點
expect(wrapper).toContainMatchingElement('div.container');
// 包含一個自定義組件
expect(wrapper).toContainMatchingElement("Image");
expect(wrapper).toContainMatchingElement('Living');
// shallow 渲染不包含子組件的內部結構
expect(wrapper).not.toContainMatchingElement('img');
// shallow 渲染包含 children 節點
expect(wrapper).toContainMatchingElement('div.text');
// shallow 渲染能夠對 children 節點內部結構作測試
expect(wrapper.find('div.text')).toIncludeText('直播中');
})
})
複製代碼
若是咱們想去測試對應組件的 props
/ state
也能夠很方便測試,不過目前存在缺陷,Class Component
能經過 toHaveProp
, toHaveState
直接測試, 可是 Hook
組件沒法測試 useState
。
it ('Image component receive props', () => {
const imageWrapper = wrapper.find('Image');、
// 對於 Hook 組件目前咱們只能測試 props
expect(imageWrapper).toHaveProp('src', avatarUrl);
})
複製代碼
wrapper.find
雖然會返回一樣的一個 ShallowWrapper
對象,可是這個對象的子結構是未展開的,若是想測試imageWrapper
內部結構,須要再 shallow render
一次。
it ('Image momponent receive props', () => {
const imageWrapper = wrapper.find('Image').shallow();
expect(imageWrapper).toHaveProp('src', avatarUrl);
expect(imageWrapper).toContainMatchingElement('img');
expect(imageWrapper.find('img')).toHaveProp('src', avatarUrl);
})
複製代碼
也能夠改變組件的 props
, 觸發組件重繪
it ('should rerender when user change', () => {
const newAvatarUrl = '' + Math.random();
wrapper.setProps({ user: { avatarUrl: newAvatarUrl }});
wrapper.update();
expect(wrapper.find('Image')).toHaveProp('src', newAvatarUrl);
})
複製代碼
另外一個常見的場景是事件模擬,事件比較接近真實測試場景,這種場景下使用 shallow
存在諸多缺陷,由於 shallow
場景事件不會像真實事件同樣有捕獲和冒泡流程,因此此時只能簡單的觸發對應的 callback
達到測試目的。
it ('will call onClick prop when click event fired', () => {
const fn = jest.fn();
wrapper.setProps({ onClick: fn });
wrapper.update();
// 這裏觸發了兩次點擊事件,可是 onClick 只會被調用一次。
wrapper.find('div.container').simulate('click');
wrapper.find('div.wrapper').simulate('click');
expect(fn).toHaveBeenCalledTimes(1);
})
複製代碼
關於這些網上有人總結了 shallow
模式下的一些不足
shallow
渲染不會進行事件冒泡,而 mount
會。shallow
渲染由於不會建立真實 DOM
,因此組件中使用 refs
的地方都沒法正常獲取,若是確實須要使用 refs
, 則必須使用 mount
。simulate
在 mount
中會更加有用,由於它會進行事件冒泡。其實上面幾點說明了一個現象是 shallow
每每只適合一種理想的場景,一些依賴瀏覽器行爲表現的操做 shallow
沒法知足,這些和真實環境相關的就只能使用mount
了。
Mount
渲染的對象結構爲 ReactWrapper
其提供了和 ShallowWrapper
幾乎同樣的 API
, 差別很小。
在 API
層面的一些差別以下
+ getDOMNode() 獲取DOM節點
+ detach() 卸載React組件,至關於 unmountComponentAtNode
+ mount() 掛載組件,unmount以後經過這個方法從新掛載
+ ref(refName) 獲取 class component 的 instance.refs 上的屬性
+ setProps(nextProps, callback)
- setProps(nextProps)
- shallow()
- dive()
- getElement()
- getElements()
複製代碼
另外因爲 mount
使用 ReactDOM
進行渲染,因此其更加接近真實場景,在這種模式下咱們能觀察到整個 DOM
結構和React組件節點結構。
describe('Mount Avatar', () => {
let wrapper = null, avatarUrl = '123';
beforeEach(() => {
wrapper = mount(<Avatar user={{ avatarUrl }} />);
})
afterEach(() => {
jest.clearAllMocks();
})
it ('should set img src with avatarurl', () => {
expect(wrapper.find('Image')).toExist();
expect(wrapper.find('Image')).toHaveProp('src', avatarUrl);
expect(wrapper.find('img')).toHaveProp('src', avatarUrl);
})
})
複製代碼
在 shallow
中沒法模擬的事件觸發問題在 mount
下就再也不是問題。
it ('will call onClick prop when click event fired', () => {
const fn = jest.fn();
wrapper.setProps({ onClick: fn });
wrapper.update();
wrapper.find('div.container').simulate('click');
wrapper.find('div.wrapper').simulate('click');
expect(fn).toHaveBeenCalledTimes(2);
})
複製代碼
總結一下 shallow
中能作的 mount
都能作,mount
中能作的 shallow
不必定能作。
render
內部使用 react-dom-server
渲染成字符串,再通過 Cherrio
轉換成內存中的結構,返回 CheerioWrapper
實例,可以完整地渲染整個DOM
樹,可是會將內部實例的狀態丟失,因此也稱爲 Static Rendering
。這種渲染可以進行的操做比較少,這裏也不做具體介紹,能夠參考 官方文檔 。
若是讓我推薦的話,對於真實瀏覽器我會推薦 Karma
+ Jasmine
方案測試,對於 React
測試 Jest
+ Enzyme
在 JSDOM
環境下已經能覆蓋大部分場景。另外測試 React
組件除了 Enzyme
提供的操做, Jest
中還有不少其餘有用的特性,好比能夠 mock
一個 npm
組件的實現,調整 setTimeout
時鐘等,真正進行單元測試時,這些工具也是必不可少的,整個單元測試技術體系包含了不少東西,本文沒法面面俱到,只介紹了一些距離咱們最近的相關的技術體系。
參考
本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!