Как cделать тестирование кода более эффективным: принципы F.I.R.S.T

44d70dff5f14538625b08c6c14848c66.png

В последнее время я все больше уделяю внимание юнит тестированию, что связано с моим наставничеством на Hexlet и выравнивание пирамиды на работе. И немного решил освежить основы при написании юнит тестов:

Быстрота (Fast)

Тесты должны выполняться очень быстро. Время выполнения, включая настройку, сам тест и завершение, должно составлять миллисекунды, так как в проекте может быть тысячи тестов.

Изоляция (Isolated/Independent)

Каждый тест должен быть независим. Он должен следовать модели «подготовка, действие, проверка» (Arrange, Act, Assert) без зависимости от других тестов или внешнего окружения.

Повторяемость (Repeatable)

Тесты должны давать одинаковые результаты в любой среде и в любое время, независимо от внешних условий, таких как дата/время или случайные значения.4

Самодостаточность (Self-Validating)

Результаты теста должны быть ясны без внешних проверок — тест либо проходит, либо нет, без всякой необходимости в дополнительной интерпретации.

Тщательность (Thorough/Timely)

Тесты должны охватывать все возможные сценарии использования и граничные условия, не ограничиваясь простым стремлением к 100% покрытию кода. Важно тестировать различные объемы данных, безопасность, и корректную обработку исключений.

Реальные примеры тестирования в React с использованием принципов FIRST

1. Быстрота (Fast)

// ❌ Плохой тест, который загружает данные из API
it('fetches and displays user data', async () => {
  render();
  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();
  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();
  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();
  expect(screen.getByText('2024-01-01')).toBeInTheDocument();
});

4. Самодостаточность (Self-Validating)

Плохой пример:

// ❌ Тест не автоматизирован полностью
it('renders correctly', () => {
  const component = render();
  console.log(component); // Требует проверки вывода
});

Хороший пример:

// ✅ Тест с проверкой
it('renders correctly', () => {
  render();
  expect(screen.getByText('Hello World')).toBeInTheDocument();
});

Этот тест полностью самодостаточен и не требует внешних действий для проверки.

5. Тщательность (Thorough/Timely)

Плохой пример:

// ❌ Тест проверяет только одно состояние компонента
it('shows loading state', () => {
  render();
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

Тест ограничен только проверкой состояния загрузки.

Хороший пример:

// ✅ Эти тесты охватывают разные возможные состояния компонента
it('shows loading state', () => {
  render();
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

it('displays data after loading', async () => {
  const mockData = { text: 'Data loaded' };
  fetchData.mockResolvedValue(mockData);
  render();
  expect(await screen.findByText('Data loaded')).toBeInTheDocument();
});

it('shows error when data fails to load', async () => {
  fetchData.mockRejectedValue('Error loading data');
  render();
  expect(await screen.findByText('Error loading data')).toBeInTheDocument();
});

Присоединяйтесь в мой tg, там я пишу различные заметки по QA

© Habrahabr.ru