搭建和配置
1.安裝依賴(lài)
npm install jest --save-dev
npm install enzyme --save-dev
npm install enzyme-to-json --save-dev //為快照提供了json的組件格式
2.package.json配置jest
setupTestFrameworkScriptFile指定enzyme初始化文件;
moduleNameMapper對(duì)css铲觉、less逼蒙、圖片等不影響JavaScript測(cè)試的靜態(tài)文件進(jìn)行mock从绘。
// package.json
"jest": {
"setupTestFrameworkScriptFile": "./setupTests.js",
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
}
}
3.enzyme初始化文件:setupTests.js
新增setupTests.js如圖:
enzyme配合react16使用的初始化配置:
// setupTests.js
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({
adapter: new Adapter()
});
4.mock文件夾
新增mock文件夾;
fileMock.js和styleMock.js分別對(duì)應(yīng)package.json中jest的配置,用來(lái)模擬css僵井、less和靜態(tài)文件赁还;
list文件夾下是列表頁(yè)的mock數(shù)據(jù),配合enzyme對(duì)列表頁(yè)進(jìn)行測(cè)試驹沿;
其他頁(yè)面的mock數(shù)據(jù)可在此自行添加。
5.gitignore
新增忽略快照文件代碼蹈胡;
在本地運(yùn)行 npm run test 后可自動(dòng)在test/snapshots下生成快照渊季。
// .gitignore
__tests__/__snapshots__/
6.安裝VScode插件jest
該插件可以方便我們不使用npm run test 也能即時(shí)看到測(cè)試結(jié)果;
view snapshot按鈕方便我們查看快照罚渐;
debug按鈕可以對(duì)測(cè)試代碼進(jìn)行調(diào)試却汉。
關(guān)于jest
具體jest文檔可參考https://jestjs.io/docs/en/api
使用jest.fn()對(duì)方法進(jìn)行mock
import Component from "../component";
const mock_fn = jest.fn();
const wrapper = mount(
< Component ></Component>
);
//使用enzyme的instance()方法將組件內(nèi)的fn()方法替換為mock_fn()
wrapper.instance().fn = mock_fn;
使用jest.spyOn()模擬跟蹤某個(gè)類(lèi)的方法的調(diào)用,如我們寫(xiě)的Mobx的store中的方法storeFn():
import store from "../store";
spy_storeFn = jest.spyOn(store, "storeFn");
//使用reactWapper.instance()獲取組件內(nèi)部方法并進(jìn)行mock
category_mount.instance().clickTextToCenter = mock_clickTextToCenter;
關(guān)于enzyme
具體enzyme文檔可參考https://airbnb.io/enzyme/
render
采用的是第三方庫(kù)Cheerio
的渲染荷并,渲染結(jié)果是普通的html結(jié)構(gòu)合砂,對(duì)于snapshot使用render
比較合適。
mount
和shallow
對(duì)組件的渲染結(jié)果不是html的dom樹(shù)源织,而是react樹(shù)翩伪,如果你chrome裝了react devtool插件,他的渲染結(jié)果就是react devtool tab下查看的組件結(jié)構(gòu)谈息,而render
的結(jié)果是element tab下查看的結(jié)果缘屹。這些只是渲染結(jié)果上的差別,更大的差別是shallow
和mount
的渲染結(jié)果是個(gè)被封裝的ReactWrapper
侠仇,可以進(jìn)行多種操作轻姿,譬如find()、parents()逻炊、children()
等選擇器進(jìn)行元素查找互亮;state()、props()
進(jìn)行數(shù)據(jù)查找余素,setState()豹休、setprops()
操作數(shù)據(jù);simulate()
模擬事件觸發(fā)等溺森。
shallow
只渲染當(dāng)前組件慕爬,只能能對(duì)當(dāng)前組件做斷言;mount
會(huì)渲染當(dāng)前組件以及所有子組件屏积,對(duì)所有子組件也可以做上述操作医窿。一般交互測(cè)試都會(huì)關(guān)心到子組件,使用的都是mount
炊林。但是mount
耗時(shí)更長(zhǎng)姥卢,內(nèi)存占用的更多,如果沒(méi)必要操作和斷言子組件,可以使用shallow
独榴。
文件引入(xxx.test.js)
首先以簡(jiǎn)單的guide組件的測(cè)試為例:
//list_guide.test.js
import React from "react"; // 必須引入react
import "../assets/configs/global_configs"; //引入全局的依賴(lài)文件以免npm run test時(shí)報(bào)錯(cuò)
import { ns } from "../src/configs/configs"; //引入組件依賴(lài)的配置文件
import store from "../src/pages/list/store"; //可以引入store僧叉,支持對(duì)store進(jìn)行操作
import mockList from "../__mocks__/list"; //引入mock數(shù)據(jù)
import { shallow, render } from "enzyme"; //引入enzyme的渲染方法
import Guide from "../src/pages/list/guide"; //引入待測(cè)的組件
import toJson from "enzyme-to-json"; // 引入enzyme-to-json為快照提供了json的組件格式
//import { BrowserRouter } from "react-router-dom";
//對(duì)于使用<Route>包裹的組件需要進(jìn)入BrowserRouter,
//否則報(bào)錯(cuò)“You should not use <Route> or withRouter() outside a <Router>”
describe("pages/list/guide",()=>{
const { setValue } = store;
it("should render without throwing an error",()=>{
setValue("guide_visible", true);
const Guide_render = render(
<Guide store ={store}/> //直接使用store將引入的store傳給待測(cè)組件
);
expect(toJson(Guide_render)).toMatchSnapshot(); //生成快照
const Guide_shallow = shallow(
<Guide store ={store}/>
);
expect(Guide_shallow.hasClass("hide")).toEqual(false);
Guide_shallow.find(`.${ns}-guide`).at(0).simulate("click");
expect(Guide_shallow.hasClass("hide")).toEqual(true);
});
});
快照測(cè)試
快照可以測(cè)試到組件的渲染結(jié)果是否與上一次生成的快照一致;
toMatchSnapshot方法會(huì)幫助我們對(duì)比這次將要生成的結(jié)構(gòu)與上次的區(qū)別棺榔;
快照測(cè)試是最簡(jiǎn)單且收益很快的測(cè)試方法瓶堕,建議每個(gè)組件都進(jìn)行快照測(cè)試。
// list_category.test.js
import "../assets/configs/global_configs";
import React from "react";
import store from "../src/pages/list/store";
import { shallow, mount, render } from "enzyme";
import Category from "../src/pages/list/category";
import { ns } from "../src/configs/configs";
import { BrowserRouter } from "react-router-dom";
import mockList from "../__mocks__/list";
import toJson from "enzyme-to-json";
describe("pages/list/category", () => {
//執(zhí)行每個(gè)用例之前清除掉所有mock
beforeEach(()=>{
jest.clearAllMocks();
});
const { setValue } = store;
it("type = thirdparty_web", () => {
setValue("category_data", mockList.category_data.thirdparty_web);
//使用render進(jìn)行快照測(cè)試症歇,直接展示的是html樹(shù)
const category_render = render(
<BrowserRouter>
<Category store={store}></Category>
</BrowserRouter>
);
//生成快照郎笆,如安裝了VScode的jest插件,這里會(huì)顯示view snapshot,點(diǎn)擊可查看快照
//toJson()將reactWrapper轉(zhuǎn)化為json格式用來(lái)生成快照
expect(toJson(category_render)).toMatchSnapshot();
//使用mount進(jìn)行交互測(cè)試
const category_mount = mount(
<BrowserRouter>
<Category store={store}></Category>
</BrowserRouter>
);
expect(category_mount.find("Router span").text()).toEqual("益智游戲");
expect(category_mount.find(".swiper-slide").at(1).text()).toEqual("推薦書(shū)籍");
});
...
});
生成快照如圖:
交互測(cè)試
主要利用enzyme的simulate()
方法來(lái)模擬事件忘晤,通過(guò)觸發(fā)事件綁定函數(shù)宛蚓,模擬事件的觸發(fā)。觸發(fā)事件后设塔,判斷props上特定函數(shù)是否被調(diào)用凄吏,傳參是否正確;組件狀態(tài)是否發(fā)生預(yù)料之中的修改闰蛔;store中的值是否按照預(yù)期變化痕钢;某個(gè)dom節(jié)點(diǎn)是否存在是否符合期望。
// list_category.test.js
import "../assets/configs/global_configs";
import React from "react";
import store from "../src/pages/list/store";
import { shallow, mount, render } from "enzyme";
import Category from "../src/pages/list/category";
import { ns } from "../src/configs/configs";
import { BrowserRouter } from "react-router-dom";
import mockList from "../__mocks__/list";
import toJson from "enzyme-to-json";
describe("pages/list/category", () => {
//執(zhí)行每個(gè)用例之前清除掉所有mock
beforeEach(()=>{
jest.clearAllMocks();
});
const { setValue } = store;
...
it("no dropdown", () => {
setValue("category_data", mockList.category_data.no_dropdown);
const category_render = render(
< Category store={store}></Category>
);
expect(toJson(category_render)).toMatchSnapshot(); // 生成快照
const category_mount = mount(
< Category store={store}></Category>
);
expect(category_mount.find(`.${ns}-swiper-container .swiper-slide`).map(node => node.text()))
.toEqual(["每月推薦", "中國(guó)影片", "歐洲電影", "亞洲電影", "國(guó)際影院", "兒童電影"]);
const
//使用jest.fn()對(duì)方法進(jìn)行mock
mock_clickTextToCenter = jest.fn(),
//使用jest.spyOn()模擬跟蹤某個(gè)類(lèi)的方法的調(diào)用
spy_showList = jest.spyOn(store, "showList");
//使用reactWapper.instance()獲取組件內(nèi)部方法并進(jìn)行mock
category_mount.instance().clickTextToCenter = mock_clickTextToCenter;
//使用simulate()觸發(fā)click事件
category_mount.find(".swiper-slide").at(3).simulate("click");
//檢測(cè)模擬的Category組件內(nèi)部的clickTextToCenter方法是否調(diào)用并且參數(shù)是3
expect(mock_clickTextToCenter).toHaveBeenCalledWith(3);
//檢測(cè)store中的current_type是否已經(jīng)變?yōu)間ood_page
expect(store.current_type).toEqual("good_page");
//檢測(cè)store中的current_id是否已經(jīng)變?yōu)?0065936895
expect(store.current_id).toEqual(30065936895);
//檢測(cè)模擬的store中的showList方法是否被調(diào)用并且參數(shù)是good_page序六,good_page盖喷,1
expect(spy_showList).toHaveBeenCalledWith("good_page", good_page, 1);
});
...
});
結(jié)語(yǔ)
本文檔還有很多不足之處,今后還會(huì)持續(xù)更新难咕。
希望大家今后在使用jest+enzyme進(jìn)行測(cè)試時(shí)有任何好的測(cè)試思路與心得體會(huì)都分享一下课梳,幫助我們一起積累react自動(dòng)化測(cè)試的經(jīng)驗(yàn),使我們的項(xiàng)目更加健壯余佃。
參考鏈接
https://jestjs.io/docs/en/api
https://airbnb.io/enzyme/
http://echizen.github.io/tech/2017/02-12-jest-enzyme-intro
http://echizen.github.io/tech/2017/02-12-jest-enzyme-setup
http://echizen.github.io/tech/2017/02-12-jest-enzyme-qa
http://echizen.github.io/tech/2017/02-12-jest-enzyme-method
http://echizen.github.io/tech/2017/04-28-jest-debug
http://echizen.github.io/tech/2017/04-24-component-lifycycle-test