Code Review и автоматизация
Качество тестов проверяется на этапе Code Review. Ревьюер обязан убедиться, что тесты устойчивы к изменениям реализации, читаемы и отражают реальное поведение системы.
Чек-лист для ревьюера
Поведение vs Реализация
- Тесты отражают поведение, а не детали реализации
- Название теста понятно без чтения кода
- Assertions проверяют видимый результат — что изменилось в UI
- Нет проверок деталей реализации — внутренних методов, приватных функций, состояния напрямую
// ❌
it('calls setIsOpen with true', () => {
const setIsOpen = vi.fn();
render(<Component setIsOpen={setIsOpen} />);
fireEvent.click(screen.getByRole('button'));
expect(setIsOpen).toHaveBeenCalledWith(true);
});
// ✅
it('shows modal when button is clicked', async () => {
render(<Component />);
await userEvent.click(screen.getByRole('button', { name: 'Open' }));
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
Селекторы
- Используются семантические селекторы —
byRole,byLabelText,byText - Нет CSS-классов как селекторов
- Нет привязки к порядку в DOM —
nth-child, индексы массивов -
byTestIdиспользуется только когда нет семантической альтернативы
// ❌
container.querySelector('.email-input');
container.querySelectorAll('input')[0];
// ✅
screen.getByRole('textbox', { name: 'Email' });
screen.getByLabelText('Email');
Асинхронность
- Асинхронные сценарии корректно ожидаются —
await,findBy*,waitFor - Нет искусственных задержек —
setTimeout,sleepзапрещены - Все промисы завершены
// ❌
await new Promise((r) => setTimeout(r, 500));
expect(screen.getByText('Saved')).toBeInTheDocument();
// ✅
expect(await screen.findByText('Saved')).toBeInTheDocument();
Изоляция и cleanup
- Между тестами очищаются: состояние, моки, подписки, таймеры
- Тесты не зависят от порядка выполнения
- Каждый тест полностью независим
beforeEach(() => {
vi.restoreAllMocks(); // включает clearAllMocks — двойной вызов не нужен
localStorage.clear();
});
// RTL вызывает cleanup() автоматически при стандартной настройке Vitest.
// Явный вызов нужен только при нестандартной конфигурации.
afterEach(() => cleanup());
Детерминированность
- Нет зависимости от текущей даты/времени — используется
vi.setSystemTime - Нет зависимости от
Math.random— значение замокировано - Нет зависимости от внешнего окружения или порядка выполнения
// ❌ Сломается через год
expect(getAge(birthDate)).toBe(30);
// ✅
vi.setSystemTime(new Date('2024-01-01'));
expect(getAge(new Date('1994-01-01'))).toBe(30);
Читаемость
- Тест понятен без знания внутренней реализации
- Snapshot-тесты используются только точечно и не заменяют явные assertions
- Предпочтителен
toMatchInlineSnapshot— snapshot виден прямо в к оде
// ❌ Только snapshot
it('renders profile', () => {
expect(render(<UserProfile user={mockUser} />).container).toMatchSnapshot();
});
// ✅ Явные assertions + inline snapshot как дополнение
it('displays user profile with correct data', () => {
render(<UserProfile user={mockUser} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('[email protected]')).toBeInTheDocument();
expect(screen.getByTestId('user-profile')).toMatchInlineSnapshot(`...`);
});
Автоматизация
ESLint
// eslint.config.js
{
files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'],
rules: {
'testing-library/prefer-find-by': 'error',
'testing-library/no-wait-for-empty-callback': 'error',
'testing-library/no-container': 'error',
'testing-library/no-node-access': 'error',
'testing-library/no-render-in-setup': 'error',
'testing-library/no-debugging-utils': 'warn',
}
}
Vitest setup
// vitest.setup.ts
beforeAll(() => {
vi.spyOn(console, 'error').mockImplementation((msg) => {
throw new Error(`Unexpected console.error: ${msg}`);
});
vi.spyOn(console, 'warn').mockImplementation((msg) => {
throw new Error(`Unexpected console.warn: ${msg}`);
});
});
beforeEach(() => {
vi.restoreAllMocks(); // включает clearAllMocks — двойной вызов не нужен
localStorage.clear();
});
// RTL вызывает cleanup() автоматически при стандартной настройке Vitest.
afterEach(() => cleanup());
MSW setup
// mocks/server.ts
import { setupServer } from 'msw/node';
export const server = setupServer();
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());