Основы юнит тестирования
на примере Реакт компонентов.
-
Виды тестов.
-
Юнит тестирование.
-
Плюсы внедрения юнит тестирование для разработчика и компании.
-
Инструментарий.
-
Примеры тестов и основные ошибки.
-
Вопросы-ответы.
Тестирование ПО — проверка соответствия между реальным поведением программы и её ожидаемым поведением на конечном наборе тестов.
Виды тестов
По степени автоматизации
- Ручное тестирование
- Автоматизированное тестирование
Виды тестов
По степени изолированности
- Тестирование компонентов
- Интеграционное тестирование
- Системное тестирование
Виды тестов
Тестирование компонентов
Позволяет проверить на корректность отдельные модули исходного кода программы.
Интеграционное тестирование
Отдельные программные модули объединяются и тестируются в группе
Системное тестирование
Выполняется на полной, интегрированной системе, с целью проверки соответствия системы исходным требованиям.
Модульное тестирование (юнит)
Автоматические регрессионные тесты компонентов ПО.
Цель
Изолировать отдельные части программы и показать, что по отдельности эти части работоспособны. Исключение регрессии.
Модульное тестирование (юнит)
Автоматические регрессионные тесты компонентов ПО.
Что требуется
Тесты для каждой нетривиальной функции или метода. Отдельные тесты для найденных багов.
Модульное тестирование (юнит)
Автоматические регрессионные тесты компонентов ПО.
Результат
Позволяет достаточно быстро проверить, не привело ли очередное изменение кода к регрессии, то есть к появлению ошибок в уже оттестированных местах программы, а также облегчает обнаружение и устранение таких ошибок.
Плюсы внедрения юнит тестирования для разработчика и компании
- Поощрение изменений
- Упрощение интеграции
- Документирование кода
- Отделение интерфейса от реализации
Трудности и проблемы
- Сложный код
- Результат известен лишь приблизительно
- Ошибки интеграции
- При общей низкой культуре программирования
- Проблемы с объектами-заглушками
Инструментарий
- Jest — запуск тестов и покрытия кода;
- Enzyme — рендер и взаимодействие с компонентами React;
- Sinon — mock-библиотека для JavaScript
- Nock — mock-библиотека для HTTP запросов
- react-test-renderer — отрисовка React компонента в JavaScript объект
Успешное прохождение тестов

Полное покрытие кода тестами

Структура хранения компонентов

Компонент
class MyComp extends PureComponent {
state = {myState: true}
componentDidMount() {
this.props.dispatch(MyActions.action('testMessage'));
this.setState({myState: false})
}
myFnc = () => {
this.props.dispatch(MyAction.action('testMessage2'))
this.setState({myState: false})
}
render(): {
return (
<div className={s.myClass} onClick={() => this.myFunc()}>
I am a button!
</div>
)
}
}
Тест компонента
MyActions.action = sinon.stub().returns({type: ''});
describe('<MyComp/>', () => {
it('should call method myFunc by click on button', () => {
const {enzymeWrapper} = shallow(<MyComp/>);
enzymeWrapper.instance().myFunc = sinon.spy();
expect(enzymeWrapper.instance().myFunc.callCount).toBe(0);
enzymeWrapper.find(`.${s.myClass}`).simulate('click');
expect(enzymeWrapper.instance().myFunc.callCount).toBe(1);
})
it('method myFunc should dispatch action and change state', () => {
const {enzymeWrapper} = shallow(<MyComp/>);
expect(MyActions.action.action.callCount).toBe(0);
expect(enzymeWrapper.state().myState).toBe(true);
enzymeWrapper.instance().myFunc();
expect(MyActions.action.action.callCount).toBe(1);
expect(MyActions.action.action.calledWithExactly('testMessage2')).toBe(true);
expect(enzymeWrapper.state().myState).toBe(false);
})
})
Результат прохождения тетста

Таблица покрытия тестами компонента

Не совпадение с исходным снимком
Исходный код
Изменёный код
render(): {
return (
<div className={s.myClass} onClick={() => this.myFunc()}>
I am a button!
</div>
)
}
render(): {
return (
<div className={s.myClass} onClick={() => this.myFunc()}>
I am a button! Plus text!
</div>
)
}
Не совпадение с исходным снимком

Распространенные ошибки при написании тестов
Связанность тестов
Результат следующего теста зависит от предыдущего
MyActions.action = sinon.stub().returns({type: ''});
describe('<MyComp/>', () => {
const {enzymeWrapper} = shallow(<MyComp/>);
it('should call method myFunc by click on button', () => {
enzymeWrapper.instance().myFunc = sinon.spy();
expect(enzymeWrapper.instance().myFunc.callCount).toBe(0);
enzymeWrapper.find(`.${s.myClass}`).simulate('click');
expect(enzymeWrapper.instance().myFunc.callCount).toBe(1);
})
it('method myFunc should dispatch action and change state', () => {
expect(MyActions.action.action.callCount).toBe(0);
expect(enzymeWrapper.state().myState).toBe(true);
enzymeWrapper.instance().myFunc();
expect(MyActions.action.action.callCount).toBe(1);
expect(MyActions.action.action.calledWithExactly('testMessage2')).toBe(true);
expect(enzymeWrapper.state().myState).toBe(false);
})
})
Связанность тестов

Не проверенны начальные значения
MyActions.action = sinon.stub().returns({type: ''});
describe('<MyComp/>', () => {
it('should call method myFunc by click on button', () => {
const {enzymeWrapper} = shallow(<MyComp/>);
enzymeWrapper.instance().myFunc = sinon.spy();
enzymeWrapper.find(`.${s.myClass}`).simulate('click');
expect(enzymeWrapper.instance().myFunc.callCount).toBe(1);
})
it('method myFunc should dispatch action and change state', () => {
const {enzymeWrapper} = shallow(<MyComp/>);
enzymeWrapper.instance().myFunc();
expect(MyActions.action.action.callCount).toBe(1);
expect(MyActions.action.action.calledWithExactly('testMessage2')).toBe(true);
expect(enzymeWrapper.state().myState).toBe(false);
})
})
Не проверенны передаваемые значения
it('method myFunc should dispatch action and change state', () => {
const {enzymeWrapper} = shallow(<MyComp/>);
expect(MyActions.action.action.callCount).toBe(0);
expect(enzymeWrapper.state().myState).toBe(true);
enzymeWrapper.instance().myFunc();
expect(MyActions.action.action.callCount).toBe(1);
expect(enzymeWrapper.state().myState).toBe(false);
})
Не проверенны все вызовы
it('method myFunc should dispatch action and change state', () => {
const {enzymeWrapper} = shallow(<MyComp/>);
expect(MyActions.action.action.callCount).toBe(0);
enzymeWrapper.instance().myFunc();
expect(MyActions.action.action.calledWithExactly('testMessage2')).toBe(true);
expect(MyActions.action.action.callCount).toBe(1);
})
Все ошибки вместе
MyActions.action = sinon.stub().returns({type: ''});
describe('<MyComp/>', () => {
const {enzymeWrapper} = shallow(<MyComp/>);
it('method myFunc should dispatch action and change state', () => {
enzymeWrapper.instance().myFunc();
expect(MyActions.action.action.callCount).toBe(1);
})
it('method componentDidMount should dispatch action', () => {
enzymeWrapper.instance().componentDidMount();
expect(MyActions.action.action.callCount).toBe(2);
})
it('should call method myFunc by click on button', () => {
enzymeWrapper.instance().myFunc = sinon.spy();
enzymeWrapper.find(`.${s.myClass}`).simulate('click');
expect(enzymeWrapper.instance().myFunc.callCount).toBe(1);
})
})
Все ошибки вместе — результат покрытия тестами

class MyComp extends PureComponent {
state = {myState: true}
componentDidMount() {
this.props.dispatch(MyActions.action('testMessage'));
this.setState({myState: false})
}
myFnc = () => {
this.props.dispatch(MyAction.action('testMessage2'))
this.setState({myState: false})
}
render(): {
return (
<div className={s.myClass} onClick={() => this.myFunc()}>
I am a button!
</div>
)
}
}
Исходный код
class MyComp extends PureComponent {
state = {myState: true}
componentDidMount() {
this.props.dispatch(MyActions.action({}));
}
myFnc = () => {
this.props.dispatch(MyAction.action(false))
this.setState({myState: -1})
}
render(): {
return (
<div className={s.myClass} onClick={() => this.myFunc()}>
I am a button!
</div>
)
}
}
Регрессия кода
Регрессия кода — результат покрытия тестами :(

Вопросы
"Unit testing - Basics" by Ivan Melnikov
By oldgraff
"Unit testing - Basics" by Ivan Melnikov
- 190