内容简介:第一次尝试将学习过的知识通过文章的方式记录下来。在写文章的过程中,发现自己更多的不足以及测试的重要性。本篇文章主要是记录使用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', }, ]); }); }); 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Pattern Recognition and Machine Learning
Christopher Bishop / Springer / 2007-10-1 / USD 94.95
The dramatic growth in practical applications for machine learning over the last ten years has been accompanied by many important developments in the underlying algorithms and techniques. For example,......一起来看看 《Pattern Recognition and Machine Learning》 这本书的介绍吧!