react前端自動化測試: jest + enzyme

1.背景


本文中的自動化測試指的是單元測試 (UT),所謂單元測試也就是對每一個單元進行測試,通俗的將通常針對的是函數,類或單個組件,不涉及系統和集成。單元測試是軟件測試的基礎測試,主要是用來驗證所測代碼是否和程序員的指望一致。css

jest 是 facebook 開源的,用來進行單元測試的框架,功能比較全面,測試、斷言、覆蓋率它均可以,另外還提供了快照功能。html

2.安裝與配置


 2.1安裝node

安裝jestreact

npm install --save-dev jest 

安裝babel-jestjquery

npm install --save-dev babel-jest

安裝enzyme,須要根據項目的react版原本安裝對應的enzymegit

npm install --save-dev enzyme enzyme-adapter-react-16

安裝react-test-renderer程序員

npm install --save-dev react-test-renderer

 2.2配置github

package.json中添加:npm

{
  "scripts": {
    "test": "jest"
  }
}

執行npm run test 命令可在終端運行查看測試運行結果。json

同時 Jest 還提供了生成測試覆蓋率報告的命令,只須要添加上 --coverage 這個參數既可生成,再加上--colors可根據覆蓋率生成不一樣顏色的報告(<50%紅色,50%~80%黃色, ≥80%綠色)

"test": "jest --colors --coverage",

.babelrc文件中添加,請根據本身的項目狀況調整

{
"env": {
    "test": {
      "presets": [["next/babel", { "preset-env": { "modules": "commonjs" }, "styled-jsx": {
        "plugins": [
          "styled-jsx-plugin-postcss"
        ]
      } }]]
    }
  }
}

jest.config.js: jest配置文件,可放在根目錄下或config文件下(也能夠起其餘名字或者直接寫在package.json裏)                                

module.exports = {
  setupFiles: ['<rootDir>/jest.setup.js'], // 運行測試前可執行的腳本(好比註冊enzyme的兼容)
  transform: {
    '^.+\\.(js|jsx|mjs)$': '<rootDir>/node_modules/babel-jest',
    '^.+\\.css$': '<rootDir>/__test__/css-transform.js',
  },
  testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'], //轉換時需忽略的文件
  testURL: 'http://localhost/', // 運行環境下的URl
};

還有一些配置, 詳細的配置見jest官網

  collectCoverage: true, // 是否收集測試時的覆蓋率信息(默認是false,同package配置的--coverage參數)
  collectCoverageFrom: ['<rootDir>/src/**/*.{js,jsx,mjs}'], // 哪些文件須要收集覆蓋率信息
  coverageDirectory: '<rootDir>/test/coverage', // 輸出覆蓋信息文件的目錄
  coveragePathIgnorePatterns: ['/node_modules/', '<rootDir>/src/index.jsx'], // 統計覆蓋信息時須要忽略的文件
  moduleNameMapper: { // 須要mock處理掉的文件,好比樣式文件 }, testMatch: [ // 匹配的測試文件 '<rootDir>/test/**/?(*.)(spec|test).{js,jsx,mjs}', '<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}', ],

jest.setup.js

/* eslint-disable import/no-extraneous-dependencies */
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

3.測試


 一般測試文件名與要測試的文件名相同,後綴爲.test.js,全部測試文件默認放在__test__文件夾中。

describe塊之中,提供測試用例的四個函數:before()、after()、beforeEach()和afterEach()。它們會在指定時間執行(若是不須要能夠不寫)

describe('加法函數測試', () => {

  before(() => {// 在本區塊的全部測試用例以前執行
  });

  after(() => {// 在本區塊的全部測試用例以後執行
  });

  beforeEach(() => {// 在本區塊的每一個測試用例以前執行
  });

  afterEach(() => {// 在本區塊的每一個測試用例以後執行
  });

it('1加1應該等於2', () => { expect(add(1, 1)).toBe(2); }); it('2加2應該等於4', () => { expect(add(2, 2)).toBe(42); });
});

測試文件中應包括一個或多個describe, 每一個describe中能夠有一個或多個it,每一個describe中能夠有一個或多個expect.

describe稱爲"測試套件"(test suite),it塊稱爲"測試用例"(test case)。

expect就是判斷源碼的實際執行結果與預期結果是否一致,若是不一致就拋出一個錯誤.

 

 3.1簡單測試

import React from 'react';

export default () => (
  <div>404</div>
);
/* eslint-env jest */
import { shallow } from 'enzyme';
import React from 'react';
import Page404 from '../components/Page404';

describe('Page404', () => {
  it('Page404 shows "404"', () => {
    const app = shallow(<Page404 />);
    expect(app.find('div').text()).toEqual('404');
  });
});

這個測試只測試了組件是否被正常顯示出來了。expect部分是斷言,實現內容是在被渲染出的Page404組件中找到div標籤,而後斷言它的text()中有沒有包含指望的文字。經過這種方式咱們能夠得知組件是否有被顯示出來。

除了text()屬性之外,還可很是靈活的經過其餘方式來得知組件是否被正常顯示。例如:

expect(wrapper.find('.card').exists()).toBeTruthy()
expect(wrapper.find('input').props().type).toBe('text')

 npm test運行全部測試文件或 npm test <name> 運行匹配的測試文件:

  • % Stmts是語句覆蓋率(statement coverage):是否每一個語句都執行了

  • % Branch分支覆蓋率(branch coverage):是否每一個分支代碼塊都執行了(if, ||, ? : )

  • % Funcs函數覆蓋率(function coverage):是否每一個函數都調用了

  • % Lines行覆蓋率(line coverage):是否每一行都執行了

在這裏簡單介紹下enzyme

enzyme是Airbnb開源的react測試類庫,提供了一套簡潔強大的API,並經過jquery風格的方式進行dom處理,開發體驗十分友好. 它提供三種測試方法

shallow:

shallow 返回組件的淺渲染,對官方shallow rendering 進行封裝。淺渲染 做用就是:它僅僅會渲染至虛擬dom,不會返回真實的dom節點,這個對測試性能有極大的提高。shallow只渲染當前組件,只能能對當前組件作斷言

 mount :

mount 方法用於將React組件加載爲真實DOM節點。mount會渲染當前組件以及全部子組件

render:

render 採用的是第三方庫Cheerio的渲染,渲染結果是普通的html結構,對於snapshot使用render比較合適。

多數狀況下,shallow 方法就能知足咱們的需求了。

 

Enzyme的一部分API,你能夠從中瞭解它的大概用法。詳細的API

.get(index):返回指定位置的子組件的DOM節點

.at(index):返回指定位置的子組件

.first():返回第一個子組件

.last():返回最後一個子組件

.type():返回當前組件的類型

.text():返回當前組件的文本內容

.html():返回當前組件的HTML代碼形式

.props():返回根組件的全部屬性

.prop(key):返回根組件的指定屬性

.state([key]):返回根組件的狀態

.setState(nextState):設置根組件的狀態

.setProps(nextProps):設置根組件的屬性

 例如:

expect(wrapper.find('input').prop('value')).toBe('default value');

 3.2 模擬 Props,渲染組件建立 Wrapper

/* eslint-env jest */
import { shallow } from 'enzyme';
import React from 'react';
import { OrderManage } from '../../components/purchaser/OrderManege';

const setup = ({ ...props }) => {
  const wrapper = shallow(<OrderManage {...props} />);
  return {
    props,
    wrapper,
  };
};
describe(
'OrderManage', () => { it('role is operator', () => { const { wrapper } = setup({ role: 'operator', isFetching: true, fetchOrdersByStatuses: () => {}, // 直接設爲空函數
    getData: jest.fn(), // Jest 提供的mock 函數 }); const params
= { node: { id: 2, }, }; expect(wrapper.instance().handlePageChange(1)); expect(wrapper.instance().OrderManagementLink(params)); expect(wrapper.find('.loader')).toHaveLength(1); expect(wrapper.find('.order-simpleGrid')).toHaveLength(0); expect(wrapper.type()).toEqual('div'); }); });

在正式測試功能以前,咱們要寫一個 setup方法用來渲染組件,由於每個測試case都會用到它

3.3 組件中的方法測試

export class Card extends React.Component {
  constructor (props) {
    super(props)

    this.cardType = 'initCard'
  }

  changeCardType (cardType) {
    this.cardType = cardType
  }
  ...
}
it('changeCardType', () => {
  let component = shallow(<Card />)
  expect(component.instance().cardType).toBe('initCard')
  component.instance().changeCardType('testCard')
  expect(component.instance().cardType).toBe('testCard')
})

其中,instance 方法能夠用於獲取組件的內部成員對象。

 3.4 模擬事件測試

 <Input value={value} onChange={e => this.handleChange(e)}/>

 

it('can save value and cancel', () => {
   const value = 'edit'
   const {wrapper, props} = setup({
      editable: true
   });
   wrapper.find('input').simulate('change', {target: {value}});
   wrapper.setProps({status: 'save'});
   expect(props.onChange).toBeCalledWith(value);
})

咱們能夠在這個返回的 dom 對象上調用相似 jquery 的api進行一些查找操做,還能夠調用 setProps 和 setState 來設置 props 和 state,也能夠用 simulate 來模擬事件,

觸發事件後,去判斷props上特定函數是否被調用,傳參是否正確;組件狀態是否發生預料之中的修改;某個dom節點是否存在是否符合指望。

例: 

wrapper.find('button').simulate('click');

wrapper.find('input').simulate('keyup');

expect(props.onClick).toBeCalled();// onClick方法被調用

expect(props.onClick).not.toBeCalled() // onClick方法沒被調用

3.5 對生命週期的測試

對於

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUnmount 

可使用 Enzyme 中的 shallow 方法加載組件,例如

  /* eslint-env jest */
  import { shallow } from 'enzyme';
 import sinon from 'sinon';
 import { App } from '../App';


it('componentWillMount', () => { sinon.spy(App.prototype, 'componentWillMount'); shallow(<App />); expect(App.prototype.componentWillMount.calledOnce).toBeTruthy(); });
it('componentWillReceiveProps', () => {
  let wrapper = shallow(<App role={''} />);
  sinon.spy(App.prototype, 'componentWillReceiveProps')
  wrapper.setProps({
    role: 'admin'
  });
 expect(App.prototype.componentWillReceiveProps.calledOnce).toBeTruthy();
})

其中,spy 是 sinon 提供的特殊函數,它能夠獲取關於函數調用的信息。例如,調用函數的次數、每次調用的參數、返回的值、拋出的錯誤等,能夠用來測試一個函數是否被正確地調用。npm i --dave-dev sinon 安裝sinon.

而對於

  • componentDidMount
  • componentDidUpdate

要用enzyme的mount方法進行加載。

3.6 使用snapshot進行UI測試

import renderer from 'react-test-renderer'

it('App -- snapshot', () => {
   const renderedValue = renderer.create(<App />).toJSON()
   expect(renderedValue).toMatchSnapshot()
})

 jest的特點, 快照測試第一次運行的時候會將 React 組件在不一樣狀況下的渲染結果(掛載前)保存一份快照文件。後面每次再運行快照測試時,都會和第一次的比較,diff出兩次快照的變化。

若是須要更新快照文件,使用  npm run test -- -u 命令

3.7 Redux測試

redux官網有詳細的例子,送上傳送門

 

4.總結


       上面主要介紹了UT的安裝配置及幾個測試demo,之前沒有接觸過單元測試,各類踩坑與啃讀API(jest + enzyme),這些demo基本能夠知足項目中的測試,後續在寫測試中再進步。剛開始接觸測試是一點思路也沒有,看見組件後無從下手,也一直在思考花費這麼多時間寫測試到底值不值得,下面是目前遇到的問題和一些思考中的問題,能夠一塊兒討論一下:

  1. 一個好測試的標準,覆蓋率越高就必定越好嗎
  2. 開發前仍是開發後測試
  3. 怎麼測純函數的組件(函數中的const以後老是執行不到)
  4. error: TypeError: Only absolute URLs are supported 未解決
相關文章
相關標籤/搜索