Одним из основных принципов эффективного тестирования в JavaScript является фокусировка на тестировании поведения, а не реализации компонента. Этот подход помогает создать более устойчивые и гибкие тесты, которые не зависят от внутренних деталей реализации и могут легко адаптироваться к изменениям в коде. В контексте тестирования с использованием библиотеки Enzyme этот принцип имеет особое значение, поскольку Enzyme предоставляет удобные средства для работы с компонентами React.
Тестирование реализации ориентировано на проверку конкретных деталей, таких как структура, внутренние состояния и способы взаимодействия различных частей приложения. Это может привести к большому количеству тестов, которые быстро становятся хрупкими при изменении этих деталей. Например, если в компоненте изменится структура состояния или его методы, то потребуется переписать множество тестов, что увеличивает время на их поддержку.
Тестирование поведения, наоборот, фокусируется на том, что компонент должен делать, а не на том, как это достигается. Это позволяет протестировать функциональность компонента с учетом того, что должно быть на выходе, независимо от того, как он достигает этого результата.
Enzyme предоставляет несколько методов для работы с компонентами React и проверки их поведения. Основные подходы включают:
simulate() и mount().setState() и отслеживание изменений.Однако важно помнить, что в большинстве случаев тесты не должны зависеть от внутренней реализации компонента. Вместо того чтобы проверять внутреннее состояние или методы компонента, тесты должны проверять результаты, которые видит пользователь, — изменения в UI, вызов внешних функций или другие побочные эффекты.
Предположим, есть компонент формы, который принимает данные пользователя и отправляет их на сервер. Тестирование поведения этого компонента будет заключаться в проверке, что форма корректно обрабатывает вводимые данные и вызывает соответствующие методы при сабмите формы, независимо от того, как именно это реализовано внутри.
class Form extends React.Component {
constructor(props) {
super(props);
this.state = { name: '', email: '' };
}
handleChange(event) {
this.setState({ [event.target.name]: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
this.props.onSubmit(this.state);
}
render() {
return (
<form onSub mit={this.handleSubmit.bind(this)}>
<input
type="text"
name="name"
value={this.state.name}
onCha nge={this.handleChange.bind(this)}
/>
<input
type="email"
name="email"
value={this.state.email}
onCha nge={this.handleChange.bind(this)}
/>
<button type="submit">Submit</button>
</form>
);
}
}
Тестирование этого компонента может быть сосредоточено на следующем поведении:
onSubmit.Тестирование будет выглядеть так:
import { shallow } from 'enzyme';
import Form from './Form';
describe('Form Component', () => {
it('should call onSubmit with correct data when form is submitted', () => {
const onSubmitM ock = jest.fn();
const wrapper = shallow(<Form onSub mit={onSubmitMock} />);
// Симулируем ввод данных
wrapper.find('input[name="name"]').simulate('change', { target: { name: 'name', value: 'John' } });
wrapper.find('input[name="email"]').simulate('change', { target: { name: 'email', value: 'john@example.com' } });
// Симулируем отправку формы
wrapper.find('form').simulate('submit', { preventDefault() {} });
// Проверяем, что onSubmit был вызван с правильными данными
expect(onSubmitMock).toHaveBeenCalledWith({ name: 'John', email: 'john@example.com' });
});
});
В этом тесте не проверяется, как именно работает состояние формы или
внутренние методы обработки событий. Вместо этого проверяется
поведение компонента: правильный вызов функции
onSubmit с ожидаемыми данными.
Для того чтобы тестировать поведение компонента в изоляции, часто
используют моки (замены реальных зависимостей) и
шпионы (проверку того, что происходило в ходе
выполнения). В приведенном примере jest.fn() создаёт шпион,
который отслеживает вызовы функции, но не выполняет её логику. Это
позволяет фокусироваться исключительно на проверке того, как компонент
взаимодействует с внешними системами.
В некоторых случаях может возникнуть соблазн протестировать детали реализации компонента. Например, можно захотеть проверить, что состояние компонента обновляется после ввода данных. Однако это не рекомендуется, поскольку такие тесты становятся уязвимыми к изменениям внутренней структуры компонента. Лучше всего тестировать только те результаты, которые видит конечный пользователь.
Пример неправильного подхода:
it('should update state on change', () => {
const wrapper = shallow(<Form onSub mit={() => {}} />);
wrapper.find('input[name="name"]').simulate('change', { target: { name: 'name', value: 'John' } });
// Неправильный тест: мы проверяем внутреннее состояние компонента
expect(wrapper.state().name).toBe('John');
});
Здесь проверяется внутреннее состояние компонента, что является примером тестирования реализации. Если в будущем структура состояния изменится, тест может сломаться, даже если поведение компонента останется корректным.
Другим важным аспектом тестирования поведения является проверка побочных эффектов. К таким эффектам можно отнести вызовы API, изменение глобального состояния или взаимодействие с другими частями приложения. Важно проверять, что эти эффекты происходят корректно, а не то, как именно они реализованы внутри компонента.
Пример теста, проверяющего вызов API:
it('should call API when form is submitted', () => {
const apiMock = jest.fn().mockResolvedValue({ success: true });
const wrapper = shallow(<Form onSub mit={apiMock} />);
// Симулируем ввод и отправку формы
wrapper.find('input[name="name"]').simulate('change', { target: { name: 'name', value: 'John' } });
wrapper.find('input[name="email"]').simulate('change', { target: { name: 'email', value: 'john@example.com' } });
wrapper.find('form').simulate('submit', { preventDefault() {} });
// Проверяем, что API был вызван с правильными данными
expect(apiMock).toHaveBeenCalledWith({ name: 'John', email: 'john@example.com' });
});
Этот тест проверяет, что при отправке формы правильно вызывается API с нужными данными. Однако внутренняя реализация вызова API скрыта, и тест проверяет только поведение компонента.
Тестирование поведения, а не реализации, — это ключевая концепция для создания устойчивых и легко поддерживаемых тестов. В Enzyme этот подход можно реализовать с помощью методов, которые позволяют симулировать взаимодействие с компонентами и проверять их поведение, не зависимо от внутренней структуры. Такой подход снижает зависимость от конкретных реализаций и помогает тестам адаптироваться к изменениям в коде.