jest+enzyme測(cè)試react組件

搭建和配置

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比較合適。

mountshallow對(duì)組件的渲染結(jié)果不是html的dom樹(shù)源织,而是react樹(shù)翩伪,如果你chrome裝了react devtool插件,他的渲染結(jié)果就是react devtool tab下查看的組件結(jié)構(gòu)谈息,而render的結(jié)果是element tab下查看的結(jié)果缘屹。這些只是渲染結(jié)果上的差別,更大的差別是shallowmount的渲染結(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末暮刃,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子爆土,更是在濱河造成了極大的恐慌椭懊,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件步势,死亡現(xiàn)場(chǎng)離奇詭異氧猬,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)坏瘩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)盅抚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人倔矾,你說(shuō)我怎么就攤上這事妄均≈拢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵丰包,是天一觀(guān)的道長(zhǎng)禁熏。 經(jīng)常有香客問(wèn)我,道長(zhǎng)邑彪,這世上最難降的妖魔是什么瞧毙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮寄症,結(jié)果婚禮上升筏,老公的妹妹穿的比我還像新娘。我一直安慰自己瘸爽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布铅忿。 她就那樣靜靜地躺著剪决,像睡著了一般。 火紅的嫁衣襯著肌膚如雪檀训。 梳的紋絲不亂的頭發(fā)上柑潦,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音峻凫,去河邊找鬼渗鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛荧琼,可吹牛的內(nèi)容都是我干的譬胎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼命锄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼堰乔!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起脐恩,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤镐侯,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后驶冒,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體苟翻,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年骗污,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了崇猫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡需忿,死狀恐怖邓尤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤汞扎,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布季稳,位于F島的核電站,受9級(jí)特大地震影響澈魄,放射性物質(zhì)發(fā)生泄漏景鼠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一痹扇、第九天 我趴在偏房一處隱蔽的房頂上張望铛漓。 院中可真熱鬧,春花似錦鲫构、人聲如沸浓恶。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)包晰。三九已至,卻和暖如春炕吸,著一層夾襖步出監(jiān)牢的瞬間伐憾,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工赫模, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留树肃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓瀑罗,卻偏偏與公主長(zhǎng)得像胸嘴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子斩祭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360