Решение для модульного тестирования React
Предварительное знание
Зачем тестировать
- Тестирование гарантирует ожидаемые результаты
- как описание поведения существующего кода
- Поощряйте разработчиков писать тестируемый код, обычно тестируемый код будет более читабельным.
- Если зависимый компонент изменен, затронутый компонент может найти ошибку в тесте.
тип теста
- Модульное тестирование: относится к тестированию программного обеспечения в единицах оригинала. Блок может быть функцией, модулем или компонентом.Основная особенность заключается в том, что пока вход остается неизменным, он должен возвращать тот же результат. Чем проще программное обеспечение для модульного тестирования, тем лучше его модульная структура и тем слабее связь между модулями. Компонентизация React и функциональное программирование, естественное для модульного тестирования
- Функциональное тестирование: Эквивалентно тестированию методом "черного ящика". Тестировщик не знает внутренней ситуации программы и не нуждается в специальных знаниях языков программирования. Он знает только ввод, вывод и функции программы. независимо от внутренней логики
- Интеграционное тестирование: на основе модульного тестирования все модули собираются в подсистемы или системы в соответствии с проектными требованиями и тестируются.
- Дымовой тест: перед формальным и всеобъемлющим тестом основные функции проверяются, чтобы подтвердить, соответствуют ли основные функции потребностям и может ли программное обеспечение работать нормально.
режим разработки
- TDD: Test-driven development, на английском языке Testing Driven Development, что подчеркивает метод разработки, который управляет всем проектом с помощью тестов, то есть сначала завершает написание теста в соответствии с интерфейсом, а затем непрерывно проходит тест, когда функция завершена, и конечная цель пройти все испытания
- BDD: Behavior-driven testing, Behavior Driven Development на английском языке, делает акцент на стиле написания тестов, то есть тесты должны быть написаны как на естественном языке, чтобы каждый участник проекта и даже продукта мог понять тест, и даже написать тест. тестовое задание
TDD и BDD имеют свои сценарии использования, BDD в целом ориентирован на автоматизированное тестирование системных функций и бизнес-логики, тогда как TDD более эффективен в процессе быстрой разработки и тестирования функциональных модулей с целью быстрого завершения разработки.
Выбор технологии: Jest + Enzyme
Jest
Jest — это среда тестирования интерфейса с открытым исходным кодом Facebook, которая в основном используется для модульного тестирования React и React Native и интегрирована в приложение create-react-app. Особенности шутки:
- Простота использования: основанный на Jasmine, он предоставляет библиотеку утверждений и поддерживает несколько стилей тестирования.
- Адаптивность: Jest является модульным, расширяемым и настраиваемым.
- Песочница и снимок: Jest имеет встроенный JSDOM, который может имитировать среду браузера и выполнять ее параллельно.
- Тестирование моментальных снимков: Jest может сериализовать дерево компонентов React, создавать соответствующие моментальные снимки строк и обеспечивать высокопроизводительное обнаружение пользовательского интерфейса путем сравнения строк.
- Система Mock: Jest реализует мощную систему Mock, которая поддерживает автоматическое и ручное создание макетов.
- Поддержка асинхронного тестирования кода: поддержка Promise и async/await.
- Автоматически генерировать результаты статического анализа: встроенный Стамбул, тестовое покрытие кода и генерировать соответствующие отчеты.
Enzyme
Enzyme – это библиотека инструментов тестирования React с открытым исходным кодом от Airbnb. Она функционирует как вторичная инкапсуляция официальной библиотеки инструментов тестирования ReactTestUtils, предоставляет набор кратких и мощных API и имеет встроенный Cheerio.
Обработка DOM реализована в стиле jQuery, и опыт разработки очень удобен. Он очень популярен в сообществе с открытым исходным кодом, а также официально рекомендован React.
Настройка тестовой среды
Установите Jest, Enzyme и babel-jest. Если версия React 15 или 16, вам необходимо установить и настроить соответствующий энзим-адаптер-реагировать-15 и энзим-адаптер-реагировать-16.
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
Добавьте «test: jest --config .jest.js» в скрипт в package.json.
.jest.js文件
module.exports = {
setupFiles: [
'./test/setup.js',
],
moduleFileExtensions: [
'js',
'jsx',
],
testPathIgnorePatterns: [
'/node_modules/',
],
testRegex: '.*\\.test\\.js$',
collectCoverage: false,
collectCoverageFrom: [
'src/components/**/*.{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|scss)$": "<rootDir>/__mocks__/styleMock.js"
},
transform: {
"^.+\\.js$": "babel-jest"
},
};
- setupFiles: файл конфигурации, перед запуском кода тестового примера Jest запустит здесь файл конфигурации, чтобы инициализировать указанную тестовую среду.
- moduleFileExtensions: представляет имена файлов, которые поддерживают загрузку
- testPathIgnorePatterns: используйте регулярные выражения для поиска непроверенных файлов.
- testRegex: тестовый файл в обычном представлении, формат тестового файла xxx.test.js
- collectCoverage: следует ли генерировать отчет о тестовом покрытии, если он включен, это увеличит время тестирования.
- collectCoverageFrom: создайте отчет о тестовом покрытии, который обнаруживает файлы покрытия.
- moduleNameMapper: представляет имя ресурса, который необходимо имитировать.
- преобразование: используйте babel-jest для компиляции файлов для генерации синтаксиса ES6/7.
Jest
globals API
- описать(имя, фн): описать блок, в котором рассказывается о группе функционально связанных тестовых случаев, сгруппированных вместе
- it(name, fn, timeout): псевдоним test, используется для размещения тестовых случаев
- afterAll(fn, timeout): метод, который будет выполняться после выполнения всех тестовых случаев.
- beforeAll(fn, timeout): метод, который должен быть выполнен до того, как будут выполнены все тестовые примеры
- afterEach(fn): метод, который будет выполняться после выполнения каждого теста.
- dobedeach (Fn): метод, который должен быть выполнен до выполнения каждого тестового корпуса
Global и Describe могут иметь указанные выше четыре функции цикла.Приоритет функции AFTER в Describe выше, чем у общей функции AFTER, а приоритет функции Before в Describe ниже, чем у глобальной функции Before.
beforeAll(() => {
console.log('global before all');
});
afterAll(() => {
console.log('global after all');
});
beforeEach(() =>{
console.log('global before each');
});
afterEach(() => {
console.log('global after each');
});
describe('test1', () => {
beforeAll(() => {
console.log('test1 before all');
});
afterAll(() => {
console.log('test1 after all');
});
beforeEach(() => {
console.log('test1 before each');
});
afterEach(() => {
console.log('test1 after each');
});
it('test sum', () => {
expect(sum(2, 3)).toEqual(5);
});
it('test mutil', () => {
expect(sum(2, 3)).toEqual(7);
});
});
config
Jest имеет множество элементов конфигурации, которые можно записать в package.json, чтобы добавить поля jest для настройки, или указать файл конфигурации через командную строку --config.
объект шутки
- jest.fn(реализация): возвращает совершенно новую фиктивную функцию, которая не использовалась.При вызове этой функции будет записано много информации, связанной с вызовом функции.
- jest.mock(moduleName, factory, options): используется для имитации некоторых модулей или файлов.
- jest.spyOn(object, methodName): возвращает фиктивную функцию, похожую на jest.fn, но способную отслеживать информацию о вызове объекта [methodName], аналогичную Sinon.
Mock Functions
С помощью mock-функции можно легко смоделировать зависимости между кодами: с помощью fn или spyOn смоделировать конкретную функцию, с помощью mocking смоделировать модуль. Конкретный API можно увидетьmock-function-api.
снимок
Моментальный снимок создаст структуру пользовательского интерфейса компонента и сохранит ее в файле __snapshots__ в виде строки.Сравнивая две строки, чтобы определить, изменился ли пользовательский интерфейс, поскольку это сравнение строк, производительность очень высока.
Чтобы использовать функцию моментального снимка, вам необходимо внедрить библиотеку react-test-renderer и использовать в ней метод renderer.Если jest обнаружит метод toMatchSnapshot во время выполнения, в том же каталоге будет сгенерирована папка __snapshots для хранения файлов моментальных снимков. Каждый тест сравнивается со снимком, созданным в первый раз. Файлы снимков можно обновить с помощью jest --updateSnapshot.
Асинхронное тестирование
Jest поддерживает асинхронное тестирование и поддерживает асинхронное тестирование двумя способами: Promise и Async/Await.
Общее утверждение
- ожидание (значение): если вы хотите проверить значение для утверждения, используйте ожидание, чтобы обернуть значение
- toBe(value): используйте Object.is для сравнения, если вы сравниваете числа с плавающей запятой, используйте toBeCloseTo
- не: используется для отрицания
- toEqual(value): для глубокого сравнения объектов
- toMatch(regexpOrString): используется для проверки соответствия строки, это может быть регулярное выражение или строка
- toContain(item): используется для определения того, находится ли элемент в массиве или нет, его также можно использовать для оценки строки
- toBeNull (значение): соответствует только нулю
- toBeUndefined (значение): соответствует только undefined
- Tobedefined (значение): в отличие от TOBEUNDEFINED
- toBeTruthy(value): соответствует любому значению, которое делает оператор if истинным
- toBeFalsy(value): соответствует любому значению, которое делает оператор if ложным
- toBeGreaterThan (число): больше, чем
- toBeGreaterThanOrEqual(число): больше или равно
- toBeLessThan(число): меньше чем
- toBeLessThanOrEqual(число): меньше или равно
- toBeInstanceOf (класс): определить, является ли он экземпляром класса
- что-либо (значение): соответствует всем значениям, кроме нулевого и неопределенного
- разрешает: используется для извлечения значения, обернутого при выполнении обещания, поддерживает цепные вызовы
- rejects: используется для извлечения значения, обернутого при отклонении промиса, поддерживает цепочку вызовов
- toHaveBeenCalled(): используется для определения того, была ли вызвана фиктивная функция.
- toHaveBeenCalledTimes(number): используется для определения количества вызовов фиктивной функции.
- Утверждения (число): Убедитесь, что есть утверждение числа называется тестовым случаем
- Расширение (Matchers): пользовательские утверждения
Enzyme
Три метода рендеринга
- Shallow: поверхностный рендеринг, который является инкапсуляцией официального Shallow Renderer. Рендеринг компонентов в виртуальные объекты DOM будет отображать только первый слой, а подкомпоненты не будут отображаться, что делает его очень эффективным. Не требует среды DOM и может использовать jQuery для доступа к информации о компонентах.
- render: статическая визуализация, которая преобразует компонент React в статическую строку HTML, а затем использует библиотеку Cheerio для анализа этой строки и возвращает объект экземпляра Cheerio, который можно использовать для анализа структуры HTML компонента.
- mount: полный рендеринг, который рендерит и загружает компонент в реальный DOM-узел, который используется для проверки взаимодействия DOM API и жизненного цикла компонента. jsdom используется для имитации среды браузера.
Среди этих трех методов мелкое и монтирование могут использовать симуляцию для интерактивного моделирования, поскольку они возвращают объекты DOM, а метод рендеринга — нет. Как правило, поверхностный метод может соответствовать требованиям.Если вам нужно оценить подкомпонент, вам нужно использовать метод рендеринга.Если вам нужно проверить жизненный цикл компонента, вам нужно использовать метод монтирования.
Общий метод
- Simulate (Event, Mock): аналоговые события, используемые для запуска событий, Event — это имя события, Mock — это объект события.
- instance(): возвращает экземпляр компонента
- find (селектор): поиск узлов в соответствии с селектором, селектор может быть селектором в CSS или конструктором компонента, отображаемым именем компонента и т. д.
- at(index): возвращает визуализированный объект
- get(index): возвращает узел реакции, чтобы протестировать его, его необходимо повторно отобразить.
- содержит (nodeOrNodes): содержит ли текущий объект ключевой узел параметра, тип параметра — объект реакции или массив объектов.
- text (): возвращает текущий текстовый компонент
- html(): возвращает форму HTML-кода текущего компонента.
- props(): возвращает все свойства корневого компонента.
- prop(key): возвращает указанное свойство корневого компонента.
- state(): возвращает состояние корневого компонента.
- setState(nextState): установить состояние корневого компонента
- setProps(nextProps): установить свойства корневого компонента
Пишите тестовые случаи
код компонента
todo-list/index.js
import React, { Component } from 'react';
import { Button } from 'antd';
export default class TodoList extends Component {
constructor(props) {
super(props);
this.handleTest2 = this.handleTest2.bind(this);
}
handleTest = () => {
console.log('test');
}
handleTest2() {
console.log('test2');
}
componentDidMount() {}
render() {
return (
<div className="todo-list">
{this.props.list.map((todo, index) => (<div key={index}>
<span className="item-text ">{todo}</span>
<Button onClick={() => this.props.deleteTodo(index)} >done</Button>
</div>))}
</div>
);
}
}
настройка тестового файла
const props = {
list: ['first', 'second'],
deleteTodo: jest.fn(),
};
const setup = () => {
const wrapper = shallow(<TodoList {...props} />);
return {
props,
wrapper,
};
};
const setupByRender = () => {
const wrapper = render(<TodoList {...props} />);
return {
props,
wrapper,
};
};
const setupByMount = () => {
const wrapper = mount(<TodoList {...props} />);
return {
props,
wrapper,
};
};
Тестирование пользовательского интерфейса с помощью снимка
it('renders correctly', () => {
const tree = renderer
.create(<TodoList {...props} />)
.toJSON();
expect(tree).toMatchSnapshot();
});
При использовании toMatchSnapshot будет сгенерирован снимок DOM компонента.Каждый раз, когда тестовый пример будет запускаться в будущем, будет сгенерирован снимок компонента и сравнен с снимком, сгенерированным в первый раз.Если структура компонента изменен, сгенерированный снимок не сможет сравниться. Тесты пользовательского интерфейса можно переделать, обновив снимок.
Протестируйте узел компонента
it('should has Button', () => {
const { wrapper } = setup();
expect(wrapper.find('Button').length).toBe(2);
});
it('should render 2 item', () => {
const { wrapper } = setupByRender();
expect(wrapper.find('button').length).toBe(2);
});
it('should render item equal', () => {
const { wrapper } = setupByMount();
wrapper.find('.item-text').forEach((node, index) => {
expect(node.text()).toBe(wrapper.props().list[index])
});
});
it('click item to be done', () => {
const { wrapper } = setupByMount();
wrapper.find('Button').at(0).simulate('click');
expect(props.deleteTodo).toBeCalled();
});
Чтобы определить, есть ли у компонента компонент Button, поскольку нет необходимости рендерить дочерние узлы, для рендеринга компонента используется поверхностный метод.Поскольку в списке реквизитов два элемента, ожидается, что должно быть два Button компоненты.
Определите, есть ли в компоненте элемент кнопки, поскольку кнопка является элементом компонента Button, все используют метод рендеринга для рендеринга, и ожидается, что он найдет даже элемент кнопки.
Определите содержимое компонента, используйте метод монтирования для рендеринга, а затем используйте forEach, чтобы определить, равно ли содержимое .item-text входящему значению. может вызываться при использовании метода deleteTodo.Определить, запускается ли событие щелчка.
жизненный цикл тестового компонента
//使用spy替身的时候,在测试用例结束后,要对spy进行restore,不然这个spy会一直存在,并且无法对相同的方法再次进行spy。
it('calls componentDidMount', () => {
const componentDidMountSpy = jest.spyOn(TodoList.prototype, 'componentDidMount');
const { wrapper } = setup();
expect(componentDidMountSpy).toHaveBeenCalled();
componentDidMountSpy.mockRestore();
});
Используйте spyOn, чтобы имитировать componentDidMount компонента, функция замены должна быть до рендеринга компонента, все функции замены должны быть определены до выполнения установки, а функция замены должна быть восстановлена после решения, в противном случае функция замены всегда будет существовать , а над имитируемой функцией нельзя снова имитировать.
Внутренняя функция тестового компонента
it('calls component handleTest', () => { // class中使用箭头函数来定义方法
const { wrapper } = setup();
const spyFunction = jest.spyOn(wrapper.instance(), 'handleTest');
wrapper.instance().handleTest();
expect(spyFunction).toHaveBeenCalled();
spyFunction.mockRestore();
});
it('calls component handleTest2', () => { //在constructor使用bind来定义方法
const spyFunction = jest.spyOn(TodoList.prototype, 'handleTest2');
const { wrapper } = setup();
wrapper.instance().handleTest2();
expect(spyFunction).toHaveBeenCalled();
spyFunction.mockRestore();
});
Используйте функцию экземпляра, чтобы получить экземпляр компонента, и используйте метод spyOn, чтобы имитировать внутренний метод экземпляра, а затем используйте этот экземпляр для вызова этого внутреннего метода. называется. Если внутренний метод определяется стрелочной функцией, экземпляр необходимо имитировать; если внутренний метод определяется обычным методом или методом связывания, то необходимо имитировать прототип компонента. На самом деле о проверке жизненного цикла или внутренних функций можно судить по каким-то изменениям состояния, потому что вызовы этих функций вообще выполняют какие-то операции над состоянием компонента.
Manual Mocks
- Чтобы вручную смоделировать глобальный модуль (moduleName), вам необходимо создать новую папку __mocks__ на уровне node_modules и создать новый файл с именем модуля в папке
- Чтобы вручную смоделировать файл (fileName), вам необходимо создать папку __mocks__ на уровне смоделированного файла, а затем создать новый файл с именем файла в папке.
add/index.js
import { add } from 'lodash';
import { multip } from '../../utils/index';
export default function sum(a, b) {
return add(a, b);
}
export function m(a, b) {
return multip(a, b);
}
add/__test__/index.test.js
import sum, { m } from '../index';
jest.mock('lodash');
jest.mock('../../../utils/index');
describe('test mocks', () => {
it('test sum', () => {
expect(sum(2, 3)).toEqual(5);
});
it('test mutilp', () => {
expect(m(2, 3)).toEqual(7);
});
});
_mocks_:
Используйте метод mock() в тестовом файле, чтобы сослаться на файл, который нужно смоделировать, Jest автоматически найдет соответствующий файл в __mocks__ и заменит его, добавление в lodash и метод multip в utils будут смоделированы в соответствующий метод. Вы можете использовать автоматический прокси для имитации библиотек асинхронных компонентов проекта (fetch, axios) или использовать fetch-mock, jest-fetch-mock для имитации асинхронных запросов.
Тестировать асинхронные методы
async/index.js
import request from './request';
export function getUserName(userID) {
return request(`/users/${userID}`).then(user => user.name);
}
async/request.js
const http = require('http');
export default function request(url) {
return new Promise((resolve) => {
// This is an example of an http request, for example to fetch
// user data from an 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));
});
});
}
mock request:
const users = {
4: {
name: 'hehe',
},
5: {
name: 'haha',
},
};
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({
error: `User with ${userID} not found.`,
});
});
});
}
request.js можно рассматривать как модуль для запроса данных. Вручную смоделируйте этот модуль, чтобы он возвращал объект Promise для асинхронной обработки.
Тестовые обещания
// 使用'.resolves'来测试promise成功时返回的值
it('works with resolves', () => {
// expect.assertions(1);
expect(user.getUserName(5)).resolves.toEqual('haha')
});
// 使用'.rejects'来测试promise失败时返回的值
it('works with rejects', () => {
expect.assertions(1);
return expect(user.getUserName(3)).rejects.toEqual({
error: 'User with 3 not found.',
});
});
// 使用promise的返回值来进行测试
it('test resolve with promise', () => {
expect.assertions(1);
return user.getUserName(4).then((data) => {
expect(data).toEqual('hehe');
});
});
it('test error with promise', () => {
expect.assertions(1);
return user.getUserName(2).catch((e) => {
expect(e).toEqual({
error: 'User with 2 not found.',
});
});
});
При тестировании промиса обязательно добавляйте возврат перед утверждением, иначе тестовая функция завершится, не дожидаясь возврата промиса. Вы можете использовать .promises/.rejects для получения возвращаемого значения или использовать метод then/catch для оценки.
Протестировать асинхронно/ожидание
// 使用async/await来测试resolve
it('works resolve with async/await', async () => {
expect.assertions(1);
const data = await user.getUserName(4);
expect(data).toEqual('hehe');
});
// 使用async/await来测试reject
it('works reject with async/await', async () => {
expect.assertions(1);
try {
await user.getUserName(1);
} catch (e) {
expect(e).toEqual({
error: 'User with 1 not found.',
});
}
});
Используйте async без возврата и используйте try/catch для перехвата исключений.
покрытие кода
Покрытие кода — это тестовая метрика, которая описывает, выполняется ли код тестового примера. Статистическое покрытие кода обычно требует помощи инструментов покрытия кода Jest интегрирует Istanbul, инструмент покрытия кода.
Четыре измерения
- Покрытие строки: выполняется ли каждая строка тестового примера
- Покрытие функций: вызывается каждая функция основного тестового примера.
- Покрытие ветвления: выполняется ли каждый блок кода IF тестового примера.
- Покрытие операторов (покрытие операторов): выполняется ли каждый оператор тестового примера
В четырех измерениях, если код написан хорошо, покрытие строк и покрытие операторов должны быть одинаковыми. Есть много ситуаций, которые вызывают покрытие филиала, в основном в том числе следующие:
- ||, &&, ? , !
- если заявление
- оператор переключения
пример
function test(a, b) {
a = a || 0;
b = b || 0;
if (a && b) {
return a + b;
} else {
return 0;
}
}
test(1, 2);
// test();
При выполнении test(1,2) покрытие кода равно
При выполнении test() покрытие кода равно
установить порог
stanbul может установить порог каждого коэффициента покрытия в командной строке, а затем проверить, соответствует ли тестовый пример стандарту.Каждый параметр связан со стандартом, и если один из них не соответствует стандарту, будет сообщено об ошибке.
Когда оператор и ветвь установлены на 90, обнаружение покрытия сообщит
{
...
"jest": {
"coverageThreshold": {
"global": {
"branches": 50,
"functions": 50,
"lines": 50,
"statements": 50
},
"./src/components/": {
"branches": 40,
"statements": 40
},
"./src/reducers/**/*.js": {
"statements": 90,
},
"./src/api/very-important-module.js": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
}
}
}
встроенный в леса
После обращения к модульному тесту в проекте я надеюсь, что каждый раз, когда вы изменяете тестируемый файл, вы сможете автоматически запускать тестовый пример перед отправкой кода, чтобы убедиться в правильности и надежности кода.
Вы можете использовать husky и lint-staged в проекте для запуска githooks, выполните некоторую проверку перед отправкой кода.
- husky: После того, как хаски будет установлен в проекте, скрипты типа pre-commit будут прописаны в .git/hooks для активациикрюк, вызванные, когда Git выполняет связанные операции
- lint-staged: staged в названии представляет промежуточную область в Git, он будет отображать только содержимое, которое будет добавлено в промежуточную область.
В package.json precommit выполняет lint-staged, настраивает lint-staged, выполняет eslint-проверку всех js-файлов и тестирует js-файлы в src/components.
{
"scripts": {
"precommit": "lint-staged",
},
"lint-staged": {
"ignore": [
"build/*",
"node_modules"
],
"linters": {
"src/*.js": [
"eslint --fix",
"git add"
],
"src/components/**/*.js": [
"jest --findRelatedTests --config .jest.js",
"git add"
]
}
},
}
Когда файлы в контейнерах изменяются, а затем помещаются в промежуточную область, проверки eslint будут выполняться, но тесты выполняться не будут.
Измените список задач в компонентах, eslint проверит и выполнит тестовый пример компонента списка задач, поскольку структура компонента изменена, поэтому сравнение пользовательского интерфейса со снимками не удастся