Перейти к основной части
Version: 26.x

Тестирование React приложений

В Facebook мы используем Jest для тестирования React приложений.

Настройка#

Настройка с Create React App#

Если вы только знакомитесь с React, мы рекомендуем использовать Create React App. Он готов к использованию и поставляется вместе с Jest! Вам понадобится только добавить react-test-renderer для рендеринга снимков.

Запуск

yarn add --dev react-test-renderer

Настройка без Create React App#

Если у вас есть существующее приложение, то вам понадобится установить несколько пакетов, чтобы все хорошо работало в совместности. Мы используем пакет babel-jest и Babel прежде для 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"
}
babel.config.js
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};

И все готово!

Тестирование при помощи снимков#

Создадим тест использующий снимок для компонента Link, который отображает гиперссылки:

Link.react.js
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;

Примечание: Данные примеры используют функциональные компоненты, но классовые компоненты могут тестироваться таким же путем. 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 для взаимодействия с компонентом и захвата результата его отображения, а также создания файла снимка:

Link.react.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link.react';
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:

__tests__/__snapshots__/Link.react.test.js.snap
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>
`;

При следующем запуске тестов, отображаемый вывод будет сравнен с сохраненным снимком. Этот снимок следует занести в систему контроля версий наряду с изменениями в коде. Когда тест использующий снимки проваливается, вам следует проверить предвиденные это изменения или нет. Если изменения предвиденные, то вы можете запустить Jest командой jest -u для перезаписи существующих снимков.

Код данного примера доступен в examples/snapshot.

Тестирование снимков с помощью Mocks, Enzyme и 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.
# Or:
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:

  1. Render as text. This way you won't see the props passed to the mock component in the snapshot, but it's straightforward: js jest.mock('./SomeComponent', () => () => 'SomeComponent');
    jest.mock('./SomeComponent', () => () => 'SomeComponent');
  2. Render as a custom element. DOM "custom elements" aren't checked for anything and shouldn't fire warnings. They are lowercase and have a dash in the name.
    tsx
    jest.mock('./Widget', () =&#062; () =&#062; <mock-widget />);
  3. Use react-test-renderer. The test renderer doesn't care about element types and will happily accept e.g. SomeComponent. You could check snapshots using the test renderer, and check component behavior separately using Enzyme.
  4. Disable warnings all together (should be done in your jest setup file):
    jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction'));
    Disable warnings all together (should be done in your jest setup file): js jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction')); This shouldn't normally be your option of choice as useful warnings could be lost. However, in some cases, for example when testing react-native's components we are rendering react-native tags into the DOM and many warnings are irrelevant. Another option is to swizzle the console.warn and suppress specific warnings.

Тестирование DOM#

Если вы хотите создавать утверждения и манипулировать отображаемыми компонентами, вы можете использовать react-testing-library, Enzyme, или React's TestUtils. Следующие два примера используют react-testing-library и Enzyme.

react-testing-library#

Вы должны запустить yarn add --dev @testing-library/react, чтобы использовать react-testing-library.

Давайте имплементируем чекбокс, который переключается между двумя лейблами:

CheckboxWithLabel.js
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;
__tests__/CheckboxWithLabel-test.js
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#

Вам надо запустить yarn add --dev enzyme для использования Enzyme. Если вы используете версию React ниже 15.5.0, то вы также должны установить react-addons-test-utils.

Давайте перепишем тест сверху, используя Enzyme вместо react-testing-library. В этом примере мы используем в shallow renderer включенный в Enzyme.

__tests__/CheckboxWithLabel-test.js
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()).toEqual('Off');
checkbox.find('input').simulate('change');
expect(checkbox.text()).toEqual('On');
});

The code for this example is available at examples/enzyme.

Пользовательские трансформаторы#

Если вам нужен более продвинутый функционал, вы также можете создать свой собственный трансформатор. Вместо использования babel-jestздесь приведен пример использования @babel/core:

custom-transformer.js
'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;
},
};

Не забудьте установить пакеты babel-core и babel-preset-jest, чтобы данный пример сработал.

Для использования с Jest, вам нужно будет добавить следующий код в вашу Jest конфигурацию: "transform": {"\\.js$": "path/to/custom-transformer.js"}.

Если вы хотите построить трансформатор с поддержкой Babel вы также можете использовать babel-jest для создания и передачи пользовательских настроек:

const babelJest = require('babel-jest');
module.exports = babelJest.createTransformer({
presets: ['my-custom-preset'],
});