Пушить в прод по пятницам

Пушим в прод по пятницам

Лучше реальные данные

Писать автотесты сложно, если стоит задача воспроизвести реальные сценарии. Автотестами мы проверяем, что обновления не повредили уже готовые фичи. Тесты воспроизводят реальные пользовательские потоки, проверяя конечные статусы интерфейса.

Чтобы максимально точно воспроизводить условия на проде, тесты должны быть без мокинга критических частей кода. При этом нужно контролировать тестовые сценарии. Чтобы балансировать эти задачи, мы используем Mock Service Worker (MSW) для моделирования ответов сети, и конструкторы данных для генерирования реалистичных входных данных.

В этой статье мы рассмотрим, как интегрировать MSW и конструкторы данных в функциональные тесты, чтобы они точнее отражали продакшен-окружения и поведение.

БЕСПЛАТНО СКАЧАТЬ КНИГИ в телеграм канале Библиотека тестировщика

Важность реальных данных

Реальные данные помогают выявить проблемы, которые могут быть упущены при использовании статических или генерированных наборов данных. Используя реалистичные данные, мы делаем тесты более надежными и подвергаем приложение испытаниям, более близким к реальному использованию.

MSW и конструкторы данных вместо моков

Например, сервер на проде возвращает:

{
  "name": "John Doe",
  "email": "john.doe@bignlarge.com",
  "avatar": "https://bignlarge.com/avatars/jonny.png",
  "title": "Account Executive"
}

Можно принять эти данные в JSON-файл и вернуть их нашему мокированному серверу:

import jsonData from './mocks.json';
import { server } from '@mocks/server';
import { rest } from 'msw';

test('should show fetched data', () => {
  server.use(
    rest.get('/endpoint', (req, res, ctx) => res(ctx.json(jsonData)))
  );
  // ... остальная часть теста (отрисовка, взаимодействие, проверка)
});

Это отлично работает. А если наше приложение по-другому обработает другого человека? Или используется другая логика обработки электронной почты? Тогда понадобятся конструкторы данных.

Создание конструкторов данных

Аналогичный паттерн Builder (упрощенная его разновидность) позволяет быстро создавать вариации тестовых данных. Создадим конструктор для создания `Person`:

import type { Person } from './api.types.ts';

function buildPerson({
  name = faker.person.fullName(),
  email = faker.internet.email({
    firstName: name.split(' ')[0],
    lastName name.split(' ')[1]
  }),
  avatar = faker.image.avatar(),
  title = faker.person.jobTitle(),
}: Partial<Person> = {}): Person {
  return { name, email, avatar, title };
}

Тестирование различных сценариев работы с данными

Используя функцию конструктора данных, мы можем быстро создавать различные тест-кейсы:

import { render, screen } from '@testing-library/react';
import { buildPerson } from './mocks/data';
import Person from './Person';

test(`should render person with Us affiliation`, () => {
  const personOfOurCompany = buildPerson({ email: 'amit@gong.io' });
  render(<Person {...personOfOurCompany} />);

  const avatar = screen.getByAltText(personOfOurCompany.name);
  expect(avatar).not.toHaveClass('affiliation-them');
  expect(avatar).toHaveClass('affiliation-us');
});

test(`should render person with Them affiliation`, () => {
  const personOfOtherCompany = buildPerson({ email: 'not.amit@other.org' });
  render(<Person {...personOfOtherCompany} />);

  const avatar = screen.getByAltText(personOfOtherCompany.name);
  expect(avatar).not.toHaveClass('affiliation-us');
  expect(avatar).toHaveClass('affiliation-them');
});

Это позволяет нам тестировать все что нужно, с разными типами данных. А если тестируемый компонент получает данные, как мы это протестируем? Мы можем разделить компонент на более простой, который просто отображает данные, и более сложный, который также получает данные, а затем отображает более простой компонент. Но нам все равно нужен полный тест потока, поэтому мы просто отправим собранные данные на наш собственный мокированный сервер:

import { rest } from 'msw';
import { render, screen } from '@testing-library/react';
import { server } from '@mocks/server';
import { buildPerson } from './mocks/data';
import Person from './Person';

test(`should render person with Us affiliation`, async () => {
  const personOfOurCompany = buildPerson({ email: 'amit@gong.io' });
  server.use(
    rest.get('/endpoint/person', (req, res, ctx) => res(ctx.json(personOfOurCompany)))
  );
  render(<Person />);

  // Заметьте, что мы сейчас ждем первой отрисовки, 
  // потому что нам нужно ждать обработки запроса
  const avatar = await screen.findByAltText(personOfOurCompany.name);
  expect(avatar).not.toHaveClass('affiliation-them');
  expect(avatar).toHaveClass('affiliation-us');
});

Теперь изменения в Person, а также в любом его внутреннем компоненте будут покрыты тестами. Нам не нужно ничего знать о том, как работает реализация (использует ли она Redux, Saga, react-query, SWR, useEffect и т. д.), достаточно того, что она получает и в итоге показывает нам аватар с ожидаемой принадлежностью. А если нужно посмотреть, что произойдет, если мы не сможем найти этого человека? Без проблем:

import { rest } from 'msw';
import { render, screen, within } from '@testing-library/react';
import { server } from '@mocks/server';
import { buildPerson } from './mocks/data';
import Person from './Person';

test(`should render person with Us affiliation`, async () => {
  const personOfOurCompany = buildPerson({ email: 'amit@gong.io' });
  const errorMessage = 'something went wrong';
  server.use(
    rest.get('/endpoint/person', (req, res, ctx) => res(ctx.status(500, 'Internal Server Error'), ctx.json({ error: errorMessage })))
  );
  render(<Person />);

  const alert = await screen.findByRole('alert', { name: errorMessage });
  expect(within(alert).getByRole('button', { name: /retry/i })).toBeInTheDocument();
});

Обработка сложных данных

С более сложными структурами данных мы используем аналогичный подход. Допустим, мы хотим создать Company. Мы используем конструкторы для создания нужных деталей, а конструктор Company затем использует их:

import { faker } from '@faker-js/faker';
import { uuid } from '@mocks/common-data';
import type { Company, Person } from './api.types.ts';

export function buildCompany({
  id = uuid(),
  name = faker.company.name(),
  employees = buildEmployees({ howMany: 50, companyName: name }),
  ...otherOverrides
}: Partial<Company> = {}): Company {
  return { id, name, employees, ...otherOverrides };
}

export function buildEmployees({
  howMany = faker.datatype.number({ min: 20, max: 1000 }),
  companyName= faker.company.name()
}: { howMany: number; companyName: string } = {}) {
  return Array.from({ length: howMany }, (_, index) => buildPerson({ company: companyName }));
}

function buildPerson({
  name = faker.name.fullName(),
  company = faker.company.name(),
  email = faker.internet.email(name.split(' ')[0], name.split(' ')[1], `${company}.com`),
  avatar = faker.internet.avatar(),
  title = faker.name.jobTitle(),
}: Partial<Person> = {}): Person {
  return { name, email, avatar, title, company };
}

Реалистичное использование и реюзабельность в разработке

Конструкторы способны улучшить процесс разработки, позволяя проверять фичи нашего веб-приложения с помощью реалистичных данных. Пример настройки обработчиков, которые можно повторно использовать в различных компонентах и историях:

import { rest } from 'msw';
import type { buildEntirePageData } from './mocks/data';

export function buildHandlers({ data = buildEntirePageData() }) {
  return [
    rest.get('/endpoint/person', (req, res, ctx) => {
      const queriedEmail = req.url.searchParams.get('email');
      const result = data.people.find(({ email }) => queriedEmail === email);
      if (!result) {
        return res(ctx.status(404, 'Not found'));
      }
      return res(ctx.json(result));
    }),
    rest.post('/endpoint/person', (req, res, ctx) => {
      const queriedEmail = req.url.searchParams.get('email');
      data.people = data.people.map(({ email, ...rest }) => {
        if (queriedEmail === email) {
          return { email, ...rest, ...(await req.json()) };
        }
        return { email, ...rest };
      });
      return res(ctx.status(200));
    }),
  ];
}

Получился полнофункциональный сервер для обслуживания всего нашего приложения.
Эти обработчики можно использовать в историях, тестах, и во время разработки.

Пример использования сетевых обработчиков в тестах:

import { rest } from 'msw';
import { render, screen, within } from '@testing-library/react';
import { server } from '@mocks/server';
import { buildEntirePageData } from './mocks/data';
import { buildHandlers } from './mocks/handlers';
import PersonPage from './PersonPage';

test(`should show whatever's needed for my page`, async () => {
  const specificData = buildEntirePageData(withOrWithoutMyOverrides);
  server.use(...buildHandlers({ data: specificData }));
  render(<PersonPage />);

  // ... взаимодействие и проверка на загрузившейся страницк
});

Также можно в истории в Storybook (плагин):

export const StoryMeta = {
  parameters: {
    msw: {
      handlers: {
        // здесь создаются дефолтные данные
        users: buildHandlers(),
      },
    },
  },
};

Почитайте на сайте MSW о том, как настроить рабочий сервер и предоставить ему сетевые обработчики.

Проблемы и решения

Известный Faker отлично подходит для генерирования больших наборов случайных удобочитаемых данных. Однако при многократном запуске он может генерировать несоответствия, что приводит к нестабильности тестов.

Есть несколько способов смягчить эту проблему:

1. Использовать библиотеку UniqueEnforcer.

2. Использовать собственный список уникальных имен.

3. Добавлять индекс при создании списков.

4. Использовать запросы getAllBy*.
, когда `getAllBy*()[0]` совпадает с первым элементом из `buildList()`.

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

Перевод статьи «Push to production on Fridays».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *