測試不只能夠發現和預防問題,還能下降風險、減小企業損失。在React中,涌現了多種測試框架,本節會對其中的Jest和Enzyme作詳細的講解。html
Jest是由Facebook開源的一個測試框架,可無縫兼容React項目,專一簡單,推崇零配置,開箱即用的宗旨,用於邏輯和組件的單元測試。它的語法和斷言與Jasmine相似,而且還集成了快照測試、Mock、覆蓋率報告等功能,支持多進程並行運行測試,在內部使用JSDOM操做DOM,JSDOM是一種模擬的DOM環境,其行爲相似於常規瀏覽器,可用來與用戶交互、在節點上派發事件等。react
1)運行ios
爲了便於運行Jest,本文使用Create React App建立項目,命令以下所示。git
npx create-react-app my-app
只要把測試文件放置在__tests__目錄內,或將它們的名稱添加.test.js或.spec.js後綴,並保存在項目的src目錄中的任何深度,就能被Jest檢測到。當運行下面的命令時,可獲得相關的測試結果。github
npm test
默認狀況下,Jest每次只運行與本次更改的文件相關的測試用例。npm
2)建立測試json
若是要建立測試用例(Test Case),那麼須要使用test()或it()函數,其第一個參數是測試名稱,第二個參數是包含測試代碼的回調函數,以下所示。axios
test("two plus two is four", () => { expect(2 + 2).toBe(4); });
expect()函數用於斷言,它能接收一個實際值,並將其做爲結果與匹配器中的指望值作比較。若是匹配失敗,那麼就會在控制檯輸出相應的錯誤提示。api
describe()函數可將測試用例進行邏輯分組,其第一個參數可定義分組的名稱,以下所示。數組
describe("my test case", () => { test("one plus one is two", () => { expect(1 + 1).toBe(2); }); test("two plus two is four", () => { expect(2 + 2).toBe(4); }); });
3)匹配器
經過匹配器(Matcher)能夠各類方式來測試代碼,例如以前示例中的toBe()就是一個匹配器,它使用Object.is()來測試精確匹配,若是要檢查對象是否相等,可改用toEqual(),以下所示。
test("object assignment", () => { const data = { name: "strick" }; data["age"] = 28; expect(data).toEqual({ name: "strick", age: 28 }); });
其它經常使用的匹配器還有區分undefined、null和布爾值、比較數字、匹配字符串、檢查數組或可迭代對象是否包含某個特定項、測試拋出的錯誤等功能。
全部的匹配器均可以經過.not取反,例如驗證toBeUndefined()不能匹配null,以下所示。
test("null is not undefined", () => { expect(null).not.toBeUndefined(); });
4)異步測試
Jest提供了多種方式來測試異步代碼,包括回調函數、Promise和Async/Await,接下來會逐個講解用法。
(1)默認狀況下,Jest測試一旦執行到末尾就會完成,例若有一個check()函數(以下所示),它能接收一個回調函數,一旦check()執行結束,此測試就會在沒有執行回調函數前結束。
function check(func) { const success = true; func(success); } test("the data is truth", () => { function callback(data) { expect(data).toBeTruthy(); } check(callback); });
若要解決此問題,可爲test()的回調函數傳遞一個名爲done的函數參數,Jest會等done()回調函數執行完後,再結束測試,以下所示。
test("the data is truth", done => { function callback(data) { expect(data).toBeTruthy(); done(); } check(callback); });
(2)當異步代碼返回Promise對象時,Jest會等待其狀態的變化。若是狀態變爲已完成,那麼得使用then()方法;若是狀態變爲已拒絕,那麼得使用catch()方法,以下所示。
//狀態爲已完成 function checkResolve() { return new Promise((resolve, reject) => { resolve(true); }); } test("the data is truth", () => { return checkResolve().then(data => { expect(data).toBeTruthy(); }); }); //狀態爲已拒絕 function checkReject() { return new Promise((resolve, reject) => { reject(false); }); } test("the data is falsity", () => { return checkReject().catch(data => { expect(data).toBeFalsy(); }); });
注意,要將Promise對象做爲test()的回調函數的返回值,以避免測試提早完成,致使沒有進行方法鏈中的斷言。
在expect語句中也可使用.resolves或.rejects兩種匹配器來處理Promise的兩種狀態,以下所示,語法更爲簡潔。
test("the data is truth", () => { expect(checkResolve()).resolves.toBeTruthy(); }); test("the data is falsity", () => { expect(checkReject()).rejects.toBeFalsy(); });
(3)在測試中使用async和await兩個關鍵字,也能夠匹配Promise對象,例如斷言checkResolve()的處理結果,以下所示。
test("the data is truth", async () => { const data = await checkResolve(); expect(data).toBeTruthy(); });
它們也能用來測試已拒絕狀態的Promise,以下所示,其中assertions()用於驗證在測試中是否執行了指定數量的斷言。
function checkError() { return new Promise((resolve, reject) => { reject(); }).catch(() => { throw "error"; }); } test("the check fails with an error", async () => { expect.assertions(1); try { await checkError(); } catch (e) { expect(e).toMatch("error"); } });
aysnc和awiat還能夠與.resolves或.rejects結合使用,以下所示。
test("the data is truth", async () => { await expect(checkResolve()).resolves.toBeTruthy(); }); test("the check fails with an error", async () => { await expect(checkError()).rejects.toMatch("error"); });
5)輔助函數
有時候,在運行測試前須要作些準備工做,而在運行測試以後又須要作些整理工做,Jest提供了四個相關的輔助函數來處理這兩類工做,以下所列。
(1)beforeAll()和afterAll()會在全部測試用例以前和以後執行一次。
(2)beforeEach()和afterEach()會在每一個測試用例以前和以後執行,而且能夠像異步測試那樣處理異步代碼。
假設在四個輔助函數中輸出各自的函數名稱,而且有兩個測試用例,以下代碼所示。
beforeAll(() => { console.log("beforeAll"); }); afterAll(() => { console.log("afterAll"); }); beforeEach(() => { console.log("beforeEach"); }); afterEach(() => { console.log("afterEach"); }); test("first", () => { expect(2).toBeGreaterThan(1); }); test("second", () => { expect(2).toBeLessThan(3); });
每次運行測試,在控制檯將依次打印出「beforeAll」,兩對「beforeEach」和「afterEach」,「afterAll」。
當經過describe()對測試用例進行分組時(以下所示),外部的beforeEach()和afterEach()會優先執行。
describe("scoped", () => { beforeEach(() => console.log("inner beforeEach")); afterEach(() => console.log("inner afterEach")); test("third", () => { expect([1, 2]).toContain(1); }); });
6)Mock
Jest內置了Mock函數,可用於擦除函數的實際實現來測試代碼之間的鏈接,捕獲函數的調用和參數、配置其返回值等。
假設要測試一個自定義的forEach()函數的內部實現,那麼可使用jest.fn()建立一個Mock函數,而後經過檢查它的mock屬性來確保回調函數是否在按預期調用,以下所示。
function forEach(items, callback) { for (let index = 0; index < items.length; index++) { callback(items[index]); } } test("forEach", () => { const mockFunc = jest.fn(x => 42 + x); forEach([0, 1], mockFunc); expect(mockFunc.mock.calls.length).toBe(2); //此Mock函數被調用了兩次 expect(mockFunc.mock.calls[0][0]).toBe(0); //第一次調用函數時的第一個參數是0 expect(mockFunc.mock.calls[1][0]).toBe(1); //第二次調用函數時的第一個參數是1 expect(mockFunc.mock.results[0].value).toBe(42); //第一次函數調用的返回值是42 });
每一個Mock函數都會包含一個特殊的mock屬性,記錄了函數如何被調用、調用時的返回值等信息,經過該屬性還能追蹤每次調用時的this的值。若是要用Mock函數注入返回值,那麼能夠像下面這樣鏈式的添加,首次調用返回10,第二次調用返回「x」,接下來的調用都返回true。其中mockName()方法可爲Mock函數命名,該名稱將在輸出的日誌中顯示,可替換掉默認的「jest.fn()」。
const myMock = jest.fn().mockName("returnValue"); myMock .mockReturnValueOnce(10) .mockReturnValueOnce("x") .mockReturnValue(true); console.log(myMock(), myMock(), myMock(), myMock()); //10, 'x', true, true
Mock函數還能夠模擬模塊,例如攔截axios請求獲得的數據,以下代碼所示,爲.get提供了一個mockResolvedValue()方法,它會返回用於測試的假數據。
import axios from "axios"; jest.mock("axios"); class Users { static all() { return axios.get("./users.json").then(resp => resp.data); } } test("should fetch users", () => { const users = [{ name: "strick" }]; const resp = { data: users }; axios.get.mockResolvedValue(resp); return Users.all().then(data => expect(data).toEqual(users)); });
原生的定時器函數測試起來並不方便,經過jest.useFakeTimers()能夠模擬定時器函數,以下所示。
function timerGame() { setTimeout(() => { console.log("start"); }, 1000); } jest.useFakeTimers(); test("setTimeout", () => { timerGame(); expect(setTimeout).toHaveBeenCalledTimes(1); //調用了1次 expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); //1秒後執行回調 });
Jest模擬出的定時器函數還有快進到正確的時間點、執行當前正在等待的定時器等功能。
7)快照測試
Jest提供的快照測試(Spapshot Testing)是一種高效的UI測試,它會將React組件序列化成純文本(即快照)並保存在硬盤中,每次測試就把當前生成的快照與保存的快照進行對比,接下來用一個例子來介紹快照測試的用法。
首先建立一個Link組件,它會渲染出一條包含onMouseEnter事件的連接,當鼠標移動到這條連接時,會改變它的class屬性。
import React from "react"; const STATUS = { HOVERED: "hovered", NORMAL: "normal" }; export default class Link extends React.Component { constructor(props) { super(props); this._onMouseEnter = this._onMouseEnter.bind(this); this.state = { class: STATUS.NORMAL }; } _onMouseEnter() { this.setState({ class: STATUS.HOVERED }); } render() { return ( <a href="#" className={this.state.class} onMouseEnter={this._onMouseEnter} > {this.props.children} </a> ); } }
而後建立測試文件spapshot.test.js,在其內部,除了要引入Link組件以外,還得引入react-test-renderer,它不依賴瀏覽器和JSDOM,可將React組件渲染成JavaScript對象(即快照)。
import React from "react"; import Link from "./Link"; import renderer from "react-test-renderer"; test("Link changes the class when hovered", () => { const component = renderer.create(<Link>Strick</Link>); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); tree.props.onMouseEnter(); //觸發事件 tree = component.toJSON(); //從新渲染 expect(tree).toMatchSnapshot(); });
在第一次運行測試時,會自動建立__snapshots__目錄,放置對應的快照文件spapshot.test.js.snap,其內容以下所示,包含兩張快照,第二張是觸發onMouseEnter事件後生成的。
exports[`Link changes the class when hovered 1`] = ` <a className="normal" href="#" onMouseEnter={[Function]} > Strick </a> `; exports[`Link changes the class when hovered 2`] = ` <a className="hovered" href="#" onMouseEnter={[Function]} > Strick </a> `;
若是要刷新保存的快照,除了手動刪除以外,還能夠經過jest -u命令實現。
Enzyme是一款用於React組件的測試框架,可處理渲染出的DOM結構,開放的API相似於jQuery的語法,提供了三種不一樣的方式來測試組件:淺層渲染(Shallow Rendering)、徹底渲染(Full Rendering)和靜態渲染(Static Rendering)。從Enzyme 3開始,在安裝Enzyme的同時,還須要安裝與React版本相對應的適配器,命令以下所示。
npm install --save enzyme enzyme-adapter-react-16
1)淺層渲染
獨立於DOM的淺層渲染只會渲染React組件的第一層,它會忽略子組件的行爲,也就不必渲染子組件了,這提供了更好的隔離性。不過淺層渲染也有它侷限性,即不支持Refs。
以上一節中的Link組件爲例,在進行Enzyme以前,須要先經過configure()函數配置適配器,而後才能經過shallow()函數淺渲染Link組件,以下所示。
import React from "react"; import { shallow, configure } from "enzyme"; import Adapter from "enzyme-adapter-react-16"; import Link from "../component/Form/Link"; configure({ adapter: new Adapter() }); test("Link changes the class after mouseenter", () => { const wrapper = shallow(<Link>Strick</Link>), a = wrapper.find("a"); expect(wrapper.text()).toEqual("Strick"); a.simulate("mouseenter"); //觸發事件 expect(a.prop("className")).toEqual("normal"); //匹配樣式 });
wrapper是一個虛擬的DOM對象,它包含多個操做DOM的方法,例如find()可根據選擇器找到指定的節點,simulate()可觸發當前節點的事件。
2)徹底渲染
mount()函數會徹底渲染接收的組件,即它的子組件也會被渲染。徹底渲染依賴JSDOM,當多個測試處理同一個DOM時,可能會相互影響,所以在測試結束後須要使用unmount()方法卸載組件。
3)靜態渲染
render()函數會靜態渲染組件,也就是將它渲染成HTML字符串,再經過Cheerio庫解析該HTML結構。Cheerio相似於JSDOM,但更輕量,可像jQuery那樣操做字符串。