内容简介:第一次尝试将学习过的知识通过文章的方式记录下来。在写文章的过程中,发现自己更多的不足以及测试的重要性。本篇文章主要是记录使用Jest + Enzyme进行React技术栈单元测试所需要掌握的基本知识以及环境搭建。测试框架、断言库(chai、expect.js、should.js、Sinon.JS等)、工具有很多,以下仅列出一些比较常见的或是本人正在使用的测试框架/工具。
第一次尝试将学习过的知识通过文章的方式记录下来。在写文章的过程中,发现自己更多的不足以及测试的重要性。本篇文章主要是记录使用Jest + Enzyme进行React技术栈单元测试所需要掌握的基本知识以及环境搭建。
常用术语
-
根据测试手段划分
黑盒 白盒 灰盒
-
根据专项划分
功能 性能 安全
-
根据测试点划分
兼容性 易用性 UI元素
为什么要测试
- 作为现有代码行为的描述。
- 提升项目的健壮性、可靠性。
- 减少项目迭代重构带来的风险。
- 促使开发者写可测试的代码。
- 依赖的组件如果有修改,受影响的组件能在测试中发现错误。
- 降低人力测试的成本、提高测试的效率。
- ......
前端测试金字塔
测试框架、断言库(chai、expect.js、should.js、Sinon.JS等)、 工具 有很多,以下仅列出一些比较常见的或是本人正在使用的测试框架/工具。
-
1、单元测试(unit tests)
-
2、快照测试(snapshot tests)
-
3、端对端测试(e2e tests)
React单元测试
-
技术选型
-
环境搭建
-
安装
Jest
npm install --save-dev jest 复制代码
-
安装
Enzyme
npm install --save-dev enzyme jest-enzyme // react适配器需要与react版本想对应 参考: https://airbnb.io/enzyme/ npm install --save-dev enzyme-adapter-react-16 // 如果使用的是16.4及以上版本的react,还可以通过安装jest-environment-enzyme来设置jest的环境 npm install --save-dev jest-environment-enzyme 复制代码
-
安装
Babel
npm install --save-dev babel-jest babel-core npm install --save-dev babel-preset-env npm install --save-dev babel-preset-react // 无所不能stage-0 npm install --save-dev babel-preset-stage-0 // 按需加载插件 npm install --save-dev babel-plugin-transform-runtime 复制代码
-
修改
package.json
// package.json { "scripts": { "test": "jest" } } 复制代码
-
安装其他需要用到的库
// 安装jquery来操作dom npm install --save jquery 复制代码
-
Jest
配置更多关于
Jest
的配置请查阅 jestjs.io/docs/zh-Han…// jest.config.js module.exports = { setupFiles: ['./jest/setup.js'], // 配置测试环境,这些脚本将在执行测试代码本身之前立即在测试环境中执行。 setupTestFrameworkScriptFile: 'jest-enzyme', // 配置测试框架 testEnvironment: 'enzyme', // 使用jest-environment-enzyme时所需的配置 testEnvironmentOptions: { enzymeAdapter: 'react16', // react适配器的版本 }, testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/src/'], // 忽略的目录 transform: { // 编译配置 '^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest', '^.+\\.(css|scss)$': '<rootDir>/jest/cssTransform.js', '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '<rootDir>/jest/fileTransform.js', }, }; 复制代码
-
使用Jest测试一个Function
// add.js const add = (a, b) => a + b; export default add; 复制代码
// __tests__/add-test.js import add from '../add'; describe('add() test:', () => { it('1+2=3', () => { expect(add(1, 2)).toBe(3); // 断言是通过的,但是如果我们传入的是string类型呢? }); }); 复制代码
// 执行Jest npm test // 或 jest add-test.js --verbose 复制代码
-
快照测试
如果想确保UI不会意外更改,快照测试就是一个非常有用的工具。
// 安装react、react-dom以及react-test-renderer npm install --save react react-dom react-test-renderer 复制代码
// components/Banner.js import React from 'react'; const Banner = ({ src }) => ( <div> <img src={src} alt="banner" /> </div> ); export default Banner; 复制代码
// __tests__/components/Banner-test.js import React from 'react'; import renderer from 'react-test-renderer'; import Banner from '../../components/Banner'; describe('<Banner />', () => { it('renders correctly', () => { const tree = renderer.create(<Banner />).toJSON(); expect(tree).toMatchSnapshot(); }); }); 复制代码
-
JSDOM (JS实现的无头浏览器)
jsdom最强大的能力是它可以在jsdom中执行脚本。这些脚本可以修改页面内容并访问jsdom实现的所有Web平台API。
// handleBtn.js const $ = require('jquery'); $('#btn').click(() => $('#text').text('click on the button')); 复制代码
// handleBtn-test.js describe('JSDOM test', () => { it('click on the button', () => { // initialization document document.body.innerHTML = '<div id="btn"><span id="text"></span></div>'; const $ = require('jquery'); require('../handleBtn'); // simulation button click $('#btn').click(); // the text is updated as expected expect($('#text').text()).toEqual('click on the button'); }); }); 复制代码
-
Mock模块
在需要Mock的模块目录下新建一个
__mocks__
目录,然后新建一样的文件名,最后在测试代码中添加上jest.mock('../moduleName')
,即可实现模块的Mock。// request.js const http = require('http'); export default function request(url) { return new Promise(resolve => { // 这是一个HTTP请求的例子, 用来从API获取用户信息 // This module is being mocked in __mocks__/request.js http.get({ path: url }, response => { let data = ''; response.on('data', _data => { data += _data; }); response.on('end', () => resolve(data)); }); }); } 复制代码
// __mocks__/request.js const users = { 4: { name: 'Mark' }, 5: { name: 'Paul' }, }; export default function request(url) { return new Promise((resolve, reject) => { const userID = parseInt(url.substr('/users/'.length), 10); process.nextTick(() => (users[userID] ? resolve(users[userID]) : reject(new Error(`User with ${userID} not found.`)))); }); } 复制代码
// __tests__/request.js jest.mock('../request.js'); import request from '../request'; describe('mock request.js', () => { it('works with async/await', async () => { expect.assertions(2); // 调用2个断言 // 正确返回的断言 const res = await request('/users/4'); expect(res).toEqual({ name: 'Mark' }); // 错误返回的断言 await expect(request('/users/41')).rejects.toThrow('User with 41 not found.'); }); }); 复制代码
-
测试组件节点
-
shallow
:浅渲染,将组件作为一个单元进行测试,并确保您的测试不会间接断言子组件的行为。支持交互模拟以及组件内部函数测试 -
render
:静态渲染,将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构。可用于子组件的判断。 -
mount
:完全渲染,完整DOM渲染非常适用于您拥有可能与DOM API交互或需要测试包含在更高阶组件中的组件的用例。依赖jsdom
库,本质上是一个完全用JS实现的无头浏览器。支持交互模拟以及组件内部函数测试
// components/List.js import React, { Component } from 'react'; export default class List extends Component { constructor(props) { super(props); this.state = { list: [1], }; } render() { const { list } = this.state; return ( <div> {list.map(item => ( <p key={item}>{item}</p> ))} </div> ); } } 复制代码
// __tests__/components/List-test.js import React from 'react'; import { shallow, render, mount } from 'enzyme'; import List from '../../components/List'; describe('<List />', () => { it('shallow:render <List /> component', () => { const wrapper = shallow(<List />); expect(wrapper.find('div').length).toBe(1); }); it('render:render <List /> component', () => { const wrapper = render(<List />); expect(wrapper.html()).toBe('<p>1</p>'); }); it('mount:allows us to setState', () => { const wrapper = mount(<List />); wrapper.setState({ list: [1, 2, 3], }); expect(wrapper.find('p').length).toBe(3); }); }); 复制代码
-
-
测试组件内部函数
// components/TodoList.js import React, { Component } from 'react'; export default class TodoList extends Component { constructor(props) { super(props); this.state = { list: [], }; } handleBtn = () => { const { list } = this.state; this.setState({ list: list.length ? [...list, list.length] : [0], }); }; render() { const { list } = this.state; return ( <div> {list.map(item => ( <p key={item}>{item}</p> ))} <button type="button" onClick={() => this.handleBtn}> add item </button> </div> ); } } 复制代码
// __tests__/components/TodoList-test.js import React from 'react'; import { shallow } from 'enzyme'; import TodoList from '../../components/TodoList'; describe('<TodoList />', () => { it('calls component handleBtn', () => { const wrapper = shallow(<TodoList />); // 创建模拟函数 const spyHandleBtn = jest.spyOn(wrapper.instance(), 'handleBtn'); // list的默认长度是0 expect(wrapper.state('list').length).toBe(0); // 首次handelBtn wrapper.instance().handleBtn(); expect(wrapper.state('list').length).toBe(1); // 模拟按钮点击 wrapper.find('button').simulate('click'); expect(wrapper.state('list').length).toBe(2); // 总共执行handleBtn函数两次 expect(spyHandleBtn).toHaveBeenCalledTimes(2); // 恢复mockFn spyHandleBtn.mockRestore(); }); }); 复制代码
-
测试代码覆盖率
- 语句覆盖率(statement coverage):是否测试用例的每个语句都执行了
- 分支覆盖率(branch coverage):是否测试用例的每个if代码块都执行了
- 函数覆盖率(function coverage):是否测试用例的每一个函数都调用了
- 行覆盖率(line coverage):是否测试用例的每一行都执行了
// jest.config.js module.exports = { collectCoverage: true, // 收集覆盖率信息 coverageThreshold: { // 设置覆盖率最低阈值 global: { branches: 50, functions: 50, lines: 50, statements: 50, }, './firstTest/components': { branches: 100, }, }, }; 复制代码
-
redux单元测试
-
安装
- redux-thunk :一个用于管理redux副作用(Side Effect,例如异步获取数据)的库
- redux-saga : redux副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
- fetch-mock :模拟fetch请求
- node-fetch :fetch-mock依赖node-fetch
- redux-mock-store : 用于测试Redux异步操作创建器和中间件的模拟存储。主要用于测试与操作相关的逻辑,而不是与reducer相关的逻辑。
- redux-actions-assertions 用于测试redux actions的断言库
npm install --save redux-thunk npm install --save redux-saga npm install --save-dev fetch-mock redux-mock-store redux-actions-assertions npm install -g node-fetch 复制代码
-
测试同步action
// actions/todoActions.js export const addTodo = text => ({ type: 'ADD_TODO', text }); export const delTodo = text => ({ type: 'DEL_TODO', text }); 复制代码
// __tests__/actions/todoActions-test.js import * as actions from '../../actions/todoActions'; describe('actions', () => { it('addTodo', () => { const text = 'hello redux'; const expectedAction = { type: 'ADD_TODO', text, }; expect(actions.addTodo(text)).toEqual(expectedAction); }); it('delTodo', () => { const text = 'hello jest'; const expectedAction = { type: 'DEL_TODO', text, }; expect(actions.delTodo(text)).toEqual(expectedAction); }); }); 复制代码
-
测试基于redux-thunk的异步action
// actions/fetchActions.js export const fetchTodosRequest = () => ({ type: 'FETCH_TODOS_REQUEST' }); export const fetchTodosSuccess = data => ({ type: 'FETCH_TODOS_SUCCESS', data, }); export const fetchTodosFailure = data => ({ type: 'FETCH_TODOS_FAILURE', data, }); export function fetchTodos() { return dispatch => { dispatch(fetchTodosRequest()); return fetch('http://example.com/todos') .then(res => res.json()) .then(body => dispatch(fetchTodosSuccess(body))) .catch(ex => dispatch(fetchTodosFailure(ex))); }; } 复制代码
// __tests__/actions/fetchActions-test.js import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import fetchMock from 'fetch-mock'; import * as actions from '../../actions/fetchActions'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); describe('fetchActions', () => { afterEach(() => { fetchMock.restore(); }); it('在获取todos之后创建FETCH_TODOS_SUCCESS', async () => { fetchMock.getOnce('/todos', { body: { todos: ['do something'] }, headers: { 'content-type': 'application/json' }, }); // 所期盼的action执行记录:FETCH_TODOS_REQUEST -> FETCH_TODOS_SUCCESS const expectedActions = [ { type: 'FETCH_TODOS_REQUEST' }, { type: 'FETCH_TODOS_SUCCESS', data: { todos: ['do something'] } }, ]; const store = mockStore({ todos: [] }); // 通过async/await来优化异步操作的流程 await store.dispatch(actions.fetchTodos()); // 断言actios是否正确执行 expect(store.getActions()).toEqual(expectedActions); }); it('在获取todos之后创建FETCH_TODOS_FAILURE', async () => { fetchMock.getOnce('/todos', { throws: new TypeError('Failed to fetch'), }); const expectedActions = [ { type: 'FETCH_TODOS_REQUEST' }, { type: 'FETCH_TODOS_FAILURE', data: new TypeError('Failed to fetch') }, ]; const store = mockStore({ todos: [] }); await store.dispatch(actions.fetchTodos()); expect(store.getActions()).toEqual(expectedActions); }); }); 复制代码
-
测试 Sagas
有两个主要的测试 Sagas 的方式:一步一步测试 saga generator function,或者执行整个 saga 并断言 side effects。
- 测试 Sagas Generator Function 中的纯函数
// sagas/uiSagas.js import { put, take } from 'redux-saga/effects'; export const CHOOSE_COLOR = 'CHOOSE_COLOR'; export const CHANGE_UI = 'CHANGE_UI'; export const chooseColor = color => ({ type: CHOOSE_COLOR, payload: { color, }, }); export const changeUI = color => ({ type: CHANGE_UI, payload: { color, }, }); export function* changeColorSaga() { const action = yield take(CHOOSE_COLOR); yield put(changeUI(action.payload.color)); } 复制代码
// __tests__/sagas/uiSagas-test.js import { put, take } from 'redux-saga/effects'; import { changeColorSaga, CHOOSE_COLOR, chooseColor, changeUI, } from '../../sagas/uiSagas'; describe('uiSagas', () => { it('changeColorSaga', () => { const gen = changeColorSaga(); expect(gen.next().value).toEqual(take(CHOOSE_COLOR)); const color = 'red'; expect(gen.next(chooseColor(color)).value).toEqual(put(changeUI(color))); }); }); 复制代码
- 测试 Sagas Generator Function 中的 side effects(副作用)
// sagas/fetchSagas.js import { put, call } from 'redux-saga/effects'; export const fetchDatasSuccess = data => ({ type: 'FETCH_DATAS_SUCCESS', data, }); export const fetchDatasFailure = data => ({ type: 'FETCH_DATAS_FAILURE', data, }); export const myFetch = (...parmas) => fetch(...parmas).then(res => res.json()); export function* fetchDatas() { try { const result = yield call(myFetch, '/datas'); yield put(fetchDatasSuccess(result)); } catch (error) { yield put(fetchDatasFailure(error)); } } 复制代码
// __tests__/sagas/fetchSagas-test.js import { runSaga } from 'redux-saga'; import { put, call } from 'redux-saga/effects'; import fetchMock from 'fetch-mock'; import { fetchDatas, fetchDatasSuccess, fetchDatasFailure, myFetch, } from '../../sagas/fetchSagas'; describe('fetchSagas', () => { afterEach(() => { fetchMock.restore(); }); // 一步步generator function 并断言 side effects it('fetchDatas success', async () => { const body = { text: 'success' }; fetchMock.get('/datas', { body, headers: { 'content-type': 'application/json' }, }); const gen = fetchDatas(); // 调用next().value来获取被yield的effect,并拿它和期望返回的effect进行比对 expect(gen.next().value).toEqual(call(myFetch, '/datas')); const result = await fetch('/datas').then(res => res.json()); expect(result).toEqual(body); // 请求成功 expect(gen.next(result).value).toEqual(put(fetchDatasSuccess(body))); }); it('fetchDatas fail', () => { const gen = fetchDatas(); expect(gen.next().value).toEqual(call(myFetch, '/datas')); // 模拟异常时的处理是否预期 const throws = new TypeError('Failed to fetch'); expect(gen.throw(throws).value).toEqual(put(fetchDatasFailure(throws))); }); // 执行整个 saga 并断言 side effects。(推荐方案) it('runSage success', async () => { const body = { text: 'success' }; fetchMock.get('/datas', { body, headers: { 'content-type': 'application/json' }, }); const dispatched = []; await runSaga({ dispatch: action => dispatched.push(action), }, fetchDatas).done; expect(dispatched).toEqual([fetchDatasSuccess(body)]); }); it('runSage fail', async () => { const throws = new TypeError('Failed to fetch'); fetchMock.get('/datas', { throws, }); const dispatched = []; await runSaga({ dispatch: action => dispatched.push(action), }, fetchDatas).done; expect(dispatched).toEqual([fetchDatasFailure(throws)]); }); }); 复制代码
-
测试 reducers
// reducers/todos.js export default function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return [ { text: action.text, }, ...state, ]; default: return state; } } 复制代码
// __tests__/reducers/todos-test.js import todos from '../../reducers/todos'; describe('reducers', () => { it('should return the initial state', () => { expect(todos(undefined, {})).toEqual([]); }); it('todos initial', () => { expect(todos([{ text: '1' }], {})).toEqual([{ text: '1' }]); }); it('should handle ADD_TODO', () => { expect(todos([], { type: 'ADD_TODO', text: 'text' })).toEqual([ { text: 'text', }, ]); }); }); 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
人人都是产品经理
苏杰 / 电子工业出版社 / 2014-9-1 / CNY 55.00
《人人都是产品经理(纪念版)》为经典畅销书《人人都是产品经理》的内容升级版本。对于大量成长起来的优秀互联网产品经理,为数不少想投身产品工作的其他岗位从业者,以及更多有志从事这一职业的学生而言,这本书曾是他们记忆深刻的启蒙读物、思想基石和行动手册。作者以分享经历与体会为出发点,以“朋友间聊聊如何做产品”的语气,将自己数年产品工作过程中学到的思维方法与做事方式,及其它们对自己的帮助,系统性地梳理为用户......一起来看看 《人人都是产品经理》 这本书的介绍吧!