В последнее время я все больше уделяю внимание юнит тестированию, что связано с моим наставничеством на Hexlet и выравнивание пирамиды на работе. И немного решил освежить основы при написании юнит тестов:
Быстрота (Fast)
Тесты должны выполняться очень быстро. Время выполнения, включая настройку, сам тест и завершение, должно составлять миллисекунды, так как в проекте может быть тысячи тестов.
Изоляция (Isolated/Independent)
Каждый тест должен быть независим. Он должен следовать модели "подготовка, действие, проверка" (Arrange, Act, Assert) без зависимости от других тестов или внешнего окружения.
Повторяемость (Repeatable)
Тесты должны давать одинаковые результаты в любой среде и в любое время, независимо от внешних условий, таких как дата/время или случайные значения.
Самодостаточность (Self-Validating)
Результаты теста должны быть ясны без внешних проверок — тест либо проходит, либо нет, без всякой необходимости в дополнительной интерпретации.
Тщательность (Thorough/Timely)
Тесты должны охватывать все возможные сценарии использования и граничные условия, не ограничиваясь простым стремлением к 100% покрытию кода. Важно тестировать различные объемы данных, безопасность, и корректную обработку исключений.
Реальные примеры тестирования в React с использованием принципов FIRST
1. Быстрота (Fast)
// ❌ Плохой тест, который загружает данные из API
it('fetches and displays user data', async () => {
render(<UserProfile userId="123" />);
await waitFor(() => expect(screen.getByText(/Username/)).toBeInTheDocument());
});
Этот тест медленный, потому что делает реальный запрос к API.
Хороший пример:
// ✅ Использование моков для ускорения тестов,
// чтобы сделать тест быстрым и независимым
jest.mock('api/userApi');
it('displays user data from mock', () => {
userApi.getUser.mockResolvedValue({ id: '123', name: 'John Doe' });
render(<UserProfile userId="123" />);
expect(screen.getByText(/John Doe/)).toBeInTheDocument();
});
2. Изоляция (Isolated/Independent)
Плохой пример:
// ❌ Эти тесты зависимы друг от друга из-за общего состояния хука
const { result } = renderHook(() => useCounter());
it('increments counter', () => {
act(() => { result.current.increment(); });
expect(result.current.count).toBe(1);
});
it('increments counter again', () => {
act(() => { result.current.increment(); });
expect(result.current.count).toBe(2);
});
Хороший пример:
// ✅ Каждый тест изолирован
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
act(() => { result.current.increment(); });
expect(result.current.count).toBe(1);
});
it('increments counter independently', () => {
const { result } = renderHook(() => useCounter());
act(() => { result.current.increment(); });
expect(result.current.count).toBe(1);
});
3. Повторяемость (Repeatable)
Плохой пример:
// ❌ Тест зависит от текущей даты
it('shows current date', () => {
render(<CurrentDateDisplay />);
const today = new Date().toISOString().slice(0, 10);
expect(screen.getByText(today)).toBeInTheDocument();
});
Этот тест даст разные результаты каждый день.
Хороший пример:
// ✅ Использование фиксированной даты в тестах,
// что обеспечивает повторяемость результатов
it('shows current date', () => {
jest.useFakeTimers().setSystemTime(new Date('2024-01-01'));
render(<CurrentDateDisplay />);
expect(screen.getByText('2024-01-01')).toBeInTheDocument();
});
4. Самодостаточность (Self-Validating)
Плохой пример:
// ❌ Тест не автоматизирован полностью
it('renders correctly', () => {
const component = render(<MyComponent />);
console.log(component); // Требует проверки вывода
});
Хороший пример:
// ✅ Тест с проверкой
it('renders correctly', () => {
render(<MyComponent />);
expect(screen.getByText('Hello World')).toBeInTheDocument();
});
Этот тест полностью самодостаточен и не требует внешних действий для проверки.
5. Тщательность (Thorough/Timely)
Плохой пример:
// ❌ Тест проверяет только одно состояние компонента
it('shows loading state', () => {
render(<DataLoader />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
Тест ограничен только проверкой состояния загрузки.
Хороший пример:
// ✅ Эти тесты охватывают разные возможные состояния компонента
it('shows loading state', () => {
render(<DataLoader />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('displays data after loading', async () => {
const mockData = { text: 'Data loaded' };
fetchData.mockResolvedValue(mockData);
render(<DataLoader />);
expect(await screen.findByText('Data loaded')).toBeInTheDocument();
});
it('shows error when data fails to load', async () => {
fetchData.mockRejectedValue('Error loading data');
render(<DataLoader />);
expect(await screen.findByText('Error loading data')).toBeInTheDocument();
});
Присоединяйтесь в мой tg, там я пишу различные заметки по QA
icya
Сначала мы пишем много тестов.
Затем замечаем, что их много и начинаем их ускорять
Пишем моки, чтобы избежать долгих запросов к различным сервисам.
Проходит время и API этих сервисов меняется, мы запускаем тесты, видим, какое всё зелёненькое и катимся в прод. Прод падает. Правим моки и чиним код.
У меня когда-то давно была похожая ситуация, код изменился, а моки мокали. Пришлось в срочном порядке чинить всё, что упало. Аккуратнее с моками
hel1n Автор
Есть разные уровни тестов, в данном контексте мы говорим о юнит тестирование, а есть еще уровень с интеграционными и сквозными тестами которые и должны отловить изменения контракта
Andrey_Solomatin
Ещё надо не увлечься и не написать тест который тестирует только мок.