Перейти к основному содержимому

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());