- PVSM.RU - https://www.pvsm.ru -
Много ли вы видели react разработчиков, которые покрывают свой код тестами? А вы-то тестируете свои? Действительно, зачем, если мы можем предсказать состояние компонента и стора? Ответ довольно прост: чтобы избежать ошибок при изменениях в проекте.
Всех, кого заинтересовало, приглашаю под кат.
Клонируйте репозиторий [1] проект в котором будути написаны тесты или попробуйте написать тесты для своего проекта.
Это будет очень простой проект.
Приложение выглядит так:
И умеет только складывать и вычитать числа, но с помощью react-redux связки.
А кто эти не только? Вот, что пишут в блоге Jest.
We feel incredibly humbled that 100+ companies have adopted Jest [2] in the last six months. Companies like Twitter, Pinterest, Paypal, nytimes, IBM (Watson), Spotify, eBay, SoundCloud, Intuit, FormidableLabs, Automattic, Trivago and Microsoft have either fully or partially switched to Jest for their JavaScript testing needs.
Большим компаниям нравится простота Jest. Вот почему его любят:
Когда Jest только появился он работал не очень быстро и был спроектирован не очень хорошо, но в 2016 году Facebook сделал огромную работу по улучшению Jest и, я думаю, в ближайшее время он станет довольно популярным.
Давайте разберемся, какие нам нужны зависимости для запуска тестов.
И это все, что вам нужно.
Добавим в package.json в scripts ”test”: “jest”. И теперь мы можем запускать тесты с помощью команды yarn test или npm test.
Jest проанализирует папку __test__ и выполнит все файлы в названии которых есть .test.js or .spec.js
После того, как мы все напишем, вот что получится.
Теперь давайте напишем пару тестов.
Существует много нежелательных тестовых примеров, которые просто проверяют наличие элемента DOM. Это на самом деле не нужно, и я просто оставил их чтобы вы знали как это делать, если есть такая задача, чтобы вы справились с этим тестом. Но я бы не советовал писать такие тесты.
Я остановлюсь только на важных тестах для каждого раздела, которые вам нужно знать. Остальное вы можете просто прочитать в документациях.
Что значит Connected component? Это компонент в котором используется connect для связи с redux. Если вы посмотрите код компонента Home, вы увидите там два export'а.
import React from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import {
addInputs,
subtractInputs,
async_addInputs
} from '../actions/calculatorActions';
const mapStateToProps = ({ output }) => ({
output
});
export class Home extends React.Component{
render(){
...
}
}
export default connect(mapStateToProps, {
addInputs,
subtractInputs,
async_addInputs
})(Home);
Первый экспорт нужен для так называемых 'глупых компонентов' и export default нужен для connected/smart component. И мы будем тестировать оба варианта для начала компонент, который не получает значения извне.
А с connect компонентом мы будет тестировать react-redux часть.
Не используйте декораторы для кода который собираетесь тестировать
@connect(mapStateToProps)
export default class Home extends React.Component{
...
Импортируем глупый компонент (компонент без connect).
import { Home } from '../src/js/components/Home'
import { shallow } from 'enzyme'
// и напишем наш тест для компонента
*********************************
describe('>>>H O M E --- Shallow Render REACT COMPONENTS',()=>{
let wrapper
const output = 10
beforeEach(()=>{
wrapper = shallow(<Home output={output}/>)
})
it('+++ render the DUMB component', () => {
expect(wrapper.length).toEqual(1)
});
it('+++ contains output', () => {
expect(wrapper.find('input[placeholder="Output"]').prop('value')).toEqual(output)
});
});
Мы используем shallow рендер из enzyme, потому что нам нужно получить только react объект компонента.
Подробно разберем следующий фрагмент
beforeEach(()=>{
wrapper = shallow(<Home output={output}/>)
})
Он означает, что перед каждым выполнением функции it() мы будем выполнять функцию, которая передана в beforeEach(), тем самым получая каждый раз обновленный компонент, как будто он отрендерился впервые.
Обратите внимание, что в Home.js наше поле вывода ожидает this.props.output, поэтому нам нужно передать prop во время тестирования.
<div>
Результат : <span id="output">{this.props.output}</span>
</div>
Теперь кое что поинтереснее, импортируем наш умный компонент в тест. Теперь импорт выглядит так.
import ConnectedHome, { Home } from '../src/js/components/Home'
И так же мы будем использовать redux-mock-store.
import configureStore from 'redux-mock-store'
Сейчас рассмотрим два варианта тестирования умных компонентов. Вам нужно будет выбрать, какой больше нравится вам.
// Вставляем store прямиком в компонент
//*****************************************************************************
describe('>>>H O M E --- REACT-REDUX (Shallow + passing the {store} directly)',()=>{
const initialState = { output:100 };
const mockStore = configureStore();
let store,container;
beforeEach(()=>{
store = mockStore(initialState);
container = shallow(<ConnectedHome store={store} /> );
})
it('+++ render the connected(SMART) component', () => {
expect(container.length).toEqual(1);
});
it('+++ check Prop matches with initialState', () => {
expect(container.prop('output')).toEqual(initialState.output);
});
В этом тесте мы проверяем соответствует ли initialState, которое получается компонент через mapStateToProps.
// Оборачиваем умный компонент в Provider и нужно полностью отрендерить компонент.
//*****************************************************************************
describe('>>>H O M E --- REACT-REDUX (Mount + wrapping in <Provider>)',()=>{
const initialState = { output:10 };
const mockStore = configureStore();
let store,wrapper;
beforeEach(()=>{
store = mockStore(initialState);
wrapper = mount( <Provider store={store}><ConnectedHome /></Provider> );
})
it('+++ render the connected(SMART) component', () => {
expect(wrapper.find(ConnectedHome).length).toEqual(1);
});
it('+++ check Prop matches with initialState', () => {
expect(wrapper.find(Home).prop('output')).toEqual(initialState.output);
});
it('+++ check action on dispatching ', () => {
let action;
store.dispatch(addInputs(500));
store.dispatch(subtractInputs(100));
action = store.getActions();
expect(action[0].type).toBe("ADD_INPUTS");
expect(action[1].type).toBe("SUBTRACT_INPUTS");
});
});
Если посмотреть на код, то мы делаем тоже самое, что и в первом тесте, плюс я еще дописал пару других сравнений, но их можно реализовать и в первом тесте.
В первом и втором варианте мы используем mock store и поэтому мы не можем зафиксировать изменения, но мы можем использовать настоящий store, без дополнительных библиотек.
//*******************************************************************************************************
describe('>>>H O M E --- REACT-REDUX (actual Store + reducers) more of Integration Testing',()=>{
const initialState = { output:10 };
let store,wrapper;
beforeEach(()=>{
store = createStore(calculatorReducers);
wrapper = mount( <Provider store={store}><ConnectedHome /></Provider> );
})
it('+++ check Prop matches with initialState', () => {
store.dispatch(addInputs(500));
expect(wrapper.find(Home).prop('output')).toBe(500);
});
});
Но так не рекомендуется делать, ведь это не часть unit тестирования.
Еще одна вещь которую я люблю в Jest это snapshot testing(тестирование снэпшотов).
Когда jest сравнивает snapshot первый раз, когда их нет, он кладет их в папку __snapshots__ рядом с вашим тестируемым файлом. Для того, чтобы сделать snapshot нам нужно для начала отрендерить компонент, для этого импортируем библиотеку react-test-renderer.
import renderer from 'react-test-renderer'
// После чего сравнить snapshot
describe('>>>H O M E --- Snapshot',()=>{
it('+++capturing Snapshot of Home', () => {
const renderedValue = renderer.create(<Home output={10}/>).toJSON()
expect(renderedValue).toMatchSnapshot();
});
});
Вот как выглядит snapshot для нашего компонента Home.js
exports[`>>>H O M E --- Snapshot +++capturing Snapshot of Home 1`] = `
<div
className="container">
<h2>
using React and Redux
</h2>
<div>
Input 1:
<input
placeholder="Input 1"
type="text" />
</div>
<div>
Input 2 :
<input
placeholder="Input 2"
type="text" />
</div>
<div>
Output :
<input
placeholder="Output"
readOnly={true}
type="text"
value={10} />
</div>
<div>
<button
id="add"
onClick={[Function]}>
Add
</button>
<button
id="subtract"
onClick={[Function]}>
Subtract
</button>
</div>
<hr />
</div>
`;
И если мы что то изменим в файле Home.js и попробуем запустить тест получим ошибку.
Для того чтобы обновить snapshot'ы нужно запустить тесты с флагом -u
jest test -u || yarn test -u
Благодаря этому нам не нужно тратить много времени на тестирование, ведь если snapshot не совпадает то это значит, что мы получим ошибку при сравнении snapshots. Snapshot не содержить props и state вашего компонента, если вам нужно протестировать их в компоненте то придется создавать два экземпляра.
Создавать snapshot можно не только для компонента, но и для reducer'а, что очень удобно.
К примеру, напишем такой тест.
import reducer from './recipe';
describe('With snapshots ', () => {
it('+++ reducer with shapshot', () => {
expect(calculatorReducers(undefined, { type: 'default' })).toMatchSnapshot();
});
it('+++ reducer with shapshot', () => {
const action = {
type: 'ADD_INPUTS',
output: 50,
};
expect(calculatorReducers(undefined, action)).toMatchSnapshot();
});
});
Получим следующее:
Теперь вы понимаете почему я упомянул это вначале.
Мы просто сравним, что возвращает ActionCreators с тем, что должно быть.
import { addInputs,subtractInputs } from '../src/js/actions/calculatorActions'
describe('>>>A C T I O N --- Test calculatorActions', ()=>{
it('+++ actionCreator addInputs', () => {
const add = addInputs(50)
expect(add).toEqual({ type:"ADD_INPUTS", output:50 })
});
it('+++ actionCreator subtractInputs', () => {
const subtract = subtractInputs(-50)
expect(subtract).toEqual({ type:"SUBTRACT_INPUTS", output:-50 })
});
});
Так же просто как actionCreators мы тестируем reducers.
import calculatorReducers from '../src/js/reducers/calculatorReducers'
describe('>>>R E D U C E R --- Test calculatorReducers',()=>{
it('+++ reducer for ADD_INPUT', () => {
let state = {output:100}
state = calculatorReducers(state,{type:"ADD_INPUTS",output:500})
expect(state).toEqual({output:500})
});
it('+++ reducer for SUBTRACT_INPUT', () => {
let state = {output:100}
state = calculatorReducers(state,{type:"SUBTRACT_INPUTS",output:50})
expect(state).toEqual({output:50})
});
});
Одно из самых важных это тестирование асинхронных действий или действий с side эффектом.
Для асинхронных действий мы будем использовать redux-thunk. Давайте посмотрим как выглядит наше асинхронное действие.
export const async_addInputs = output => dispatch =>
new Promise((res, rej) => {
setTimeout(() => res(output), 3000);
}).then(res => dispatch(addInputs(res)));
А теперь самое время написать для этого jest тест. Нужно импортировать все то, что нужно.
import {
addInputs,
subtractInputs,
async_addInputs
} from '../src/js/actions/calculatorActions';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const mockStore = configureMockStore([ thunk ]);
Теперь напишем наш тест.
describe('>>>Async action --- Test calculatorActions', () => {
it('+++ thunk async_addInputs', async () => {
const store = mockStore({ output: 0 });
await store.dispatch(async_addInputs(50));
expect(store.getActions()[0]).toEqual({ type: 'ADD_INPUTS', output: 50 });
});
});
Посмотрите на функцию async_addInputs, мы дожидаемся окончания действия и возвращаем ответ, который нам нужно протестировать(В данном случае успешный). Тем самым мы можем утверждать, что было вызвано всего одно и только одно действие, а это: ADD_INPUTS.
Пойдем дальше и можно в тестах проверить примитивную систему кеширования.
(просто пример не из проекта)
it('does check if we already fetched that id and only calls fetch if necessary', () => {
const store = mockStore({id: 1234, isFetching: false }});
window.fetch = jest.fn().mockImplementation(() => Promise.resolve());
store.dispatch(fetchData(1234)); // Same id
expect(window.fetch).not.toBeCalled();
store.dispatch(fetchData(1234 + 1)); // Different id
expect(window.fetch).toBeCalled();
});
Как видно выше, id 1234 уже есть в store и нам больше не нужно получать данные с запросом на данный id.
Здесь мы рассмотрели самые базовые тесты для асинхронных действий. Так же в вашем приложении могут быть с другими side эффектами. Например работа с базой напрямую, как firebase или работа с другими api напрямую.
Отчет о покрытии тестами поддерживается из коробки. Для того, что бы увидеть статистику нужно запустить тесты с флагом --coverage
yarn test -- --coverage || npm test -- --coverage
Выглядеть он будет примерно так:
Если проверить папку с проектом, можно будет обнаружить, папку coverage, где лежит файл index.html в котором отчет для отображения в браузере.
Нажмите на файл и посмотрите подробную статистику по покрытию тестами.
Вот и все основные моменты в тестировании на jest. Все остальное вы уже после освоения этого можете посмотреть в документации, для того чтобы расширить свой кругозор по тестированию.
Автор: merrick_krg
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/267170
Ссылки в тексте:
[1] репозиторий: https://github.com/MerrickGit/reactreduxtest
[2] have adopted Jest: https://twitter.com/cpojer/status/803965499407290369
[3] Источник: https://habrahabr.ru/post/340514/
Нажмите здесь для печати.