Тестування React додатків
Ми використовуємо Jest в Facebook для тестування React додатків.
Налаштування
Налаштування з Create React App
Якщо ви тільки починаєте знайомитися з React, рекомендуємо скористатись Create React App. Це готовий до використання інструмент і він поставляється з Jest! Вам лише потрібно буде додати react-test-renderer
для рендерингу знімків.
Запустіть
yarn add --dev react-test-renderer
Налаштування без Create React App
Якщо у вас вже є існуючий додаток, вам потрібно встановити кілька пакунків, щоб змусити все працювати. Ми використовуємо пакет babel-jest
і babel-preset-react
для трансформації нашого коду в тестовому середовищі. Також перегляньте використання babel.
Запустіть
yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
Ваш package.json
повинен виглядати приблизно так (де <current-version>
- це останні актуальні версії пакетів). Додайте необхідні скрипти і конфігурацію Jest:
"dependencies": {
"react": "<current-version>",
"react-dom": "<current-version>"
},
"devDependencies": {
"@babel/preset-env": "<current-version>",
"@babel/preset-react": "<current-version>",
"babel-jest": "<current-version>",
"jest": "<current-version>",
"react-test-renderer": "<current-version>"
},
"scripts": {
"test": "jest"
}
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
І все готово!
Тестування з допомогою знімків
Давайте створимо тест, що використовує знімок для компонента Link, який відображає гіперпосилання:
import React, {useState} from 'react';
const STATUS = {
HOVERED: 'hovered',
NORMAL: 'normal',
};
const Link = ({page, children}) => {
const [status, setStatus] = useState(STATUS.NORMAL);
const onMouseEnter = () => {
setStatus(STATUS.HOVERED);
};
const onMouseLeave = () => {
setStatus(STATUS.NORMAL);
};
return (
<a
className={status}
href={page || '#'}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</a>
);
};
export default Link;
Приклади використовують функціональні компоненти, проте класові компоненти можна перевірити аналогічно. See React: Function and Class Components. Reminders that with Class components, we expect Jest to be used to test props and not methods directly.
Тепер давайте використаємо тестовий рендерер React і функцію створення знімків Jest для взаємодії з компонентом, отримання результату його відображення і створення файла знімку:
import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link';
test('Link changes the class when hovered', () => {
const component = renderer.create(
<Link page="http://www.facebook.com">Facebook</Link>,
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
tree.props.onMouseEnter();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
tree.props.onMouseLeave();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
When you run yarn test
or jest
, this will produce an output file like this:
exports[`Link changes the class when hovered 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}>
Facebook
</a>
`;
exports[`Link changes the class when hovered 2`] = `
<a
className="hovered"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}>
Facebook
</a>
`;
exports[`Link changes the class when hovered 3`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}>
Facebook
</a>
`;
Наступного разу, під час запуску тестів, результат роботи компонента буде порівняний зі збереженим знімком. The snapshot should be committed along with code changes. Коли тест, що використовує знімки, провалиться, вам потрібно буде перевірити чи відбулися навмисні, чи ненавмисні зміни. Якщо зміни були очікувані, ви можете запустити Jest командою jest -u
, щоб оновити існуючі знімки.
The code for this example is available at examples/snapshot.
Snapshot Testing with Mocks, Enzyme and React 16
There's a caveat around snapshot testing when using Enzyme and React 16+. If you mock out a module using the following style:
jest.mock('../SomeDirectory/SomeComponent', () => 'SomeComponent');
Then you will see warnings in the console:
Warning: <SomeComponent /> is using uppercase HTML. Always use lowercase HTML tags in React.
# Або:
Warning: The tag <SomeComponent> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
React 16 triggers these warnings due to how it checks element types, and the mocked module fails these checks. Your options are:
- Рендер у вигляді тексту. Таким чином, ви не побачите реквізитів в компоненті-імітації в знімку, але це прямий підхід:
jest.mock('./SomeComponent', () => () => 'SomeComponent');
- Рендер у вигляді користувацького елемента. "Користувацькі елементи" DOM нічого не перевіряють та не повинні викликати попередження. Вони мають нижній регістр і тире в імені.
jest.mock('./Widget', () => () => <mock-widget />);
- Використання
react-test-renderer
. Рендер тесту не дбає про типи елементів і без проблем прийме, наприклад,SomeComponent
. Ви можете перевірити знімки, використовуючи рендер тесту, та окремо перевірити поведінку компоненту за допомогою Enzyme. - Повне вимкнення застережень (має бути виконано в файлі налаштувань Jest):Зазвичай, вам варто уникати цього варіанту, оскільки можна втратити корисні попередження. Однак, в деяких випадках, наприклад, при тестуванні компонентів react-native, ми рендеримо теги react-native всередині DOM і більшість попереджень не мають значення. Іншим варіантом є встановлення console.warn та приховування конкретних попереджень.
jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction'));
DOM тестування
If you'd like to assert, and manipulate your rendered components you can use react-testing-library, Enzyme, or React's TestUtils. The following two examples use react-testing-library and Enzyme.
react-testing-library
You have to run yarn add --dev @testing-library/react
to use react-testing-library.
Let's implement a checkbox which swaps between two labels:
import React, {useState} from 'react';
const CheckboxWithLabel = ({labelOn, labelOff}) => {
const [isChecked, setIsChecked] = useState(false);
const onChange = () => {
setIsChecked(!isChecked);
};
return (
<label>
<input type="checkbox" checked={isChecked} onChange={onChange} />
{isChecked ? labelOn : labelOff}
</label>
);
};
export default CheckboxWithLabel;
import React from 'react';
import {cleanup, fireEvent, render} from '@testing-library/react';
import CheckboxWithLabel from '../CheckboxWithLabel';
// Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher
// unmount and cleanup DOM after the test is finished.
afterEach(cleanup);
it('CheckboxWithLabel changes the text after click', () => {
const {queryByLabelText, getByLabelText} = render(
<CheckboxWithLabel labelOn="On" labelOff="Off" />,
);
expect(queryByLabelText(/off/i)).toBeTruthy();
fireEvent.click(getByLabelText(/off/i));
expect(queryByLabelText(/on/i)).toBeTruthy();
});
The code for this example is available at examples/react-testing-library.
Enzyme
You have to run yarn add --dev enzyme
to use Enzyme. If you are using a React version below 15.5.0, you will also need to install react-addons-test-utils
.
Let's rewrite the test from above using Enzyme instead of react-testing-library. We use Enzyme's shallow renderer in this example.
import React from 'react';
import {shallow} from 'enzyme';
import CheckboxWithLabel from '../CheckboxWithLabel';
test('CheckboxWithLabel changes the text after click', () => {
// Render a checkbox with label in the document
const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);
expect(checkbox.text()).toBe('Off');
checkbox.find('input').simulate('change');
expect(checkbox.text()).toBe('On');
});
Власні перетворювачі коду
If you need more advanced functionality, you can also build your own transformer. Instead of using babel-jest
, here is an example of using @babel/core
:
'use strict';
const {transform} = require('@babel/core');
const jestPreset = require('babel-preset-jest');
module.exports = {
process(src, filename) {
const result = transform(src, {
filename,
presets: [jestPreset],
});
return result || src;
},
};
Don't forget to install the @babel/core
and babel-preset-jest
packages for this example to work.
Щоб це запрацювало з Jest вам необхідно оновити вашу Jest конфігурацію цим: "transform": {"\\.js$": "path/to/custom-transformer.js"}
.
If you'd like to build a transformer with babel support, you can also use babel-jest
to compose one and pass in your custom configuration options:
const babelJest = require('babel-jest');
module.exports = babelJest.createTransformer({
presets: ['my-custom-preset'],
});
See dedicated docs for more details.