Тестирование компонента React, получающего данные

Тестирование компонента React, получающего данные

Мы будем использовать Mock Service Worker для имитации API. Поэтому вместо прямого вызова API или имитации window.fetch лучше имитировать поведение сервера.

Это дает нам песочницу для игры с сервером API без загрязнения window.fetch и без необходимости устанавливать тестовый сервер API для тестирования.

Спасибо Артему Захарченко, который сделал полезный инструмент.

Введение

Вы создали красивую функцию Feed на своем сайте и теперь хотите протестировать эту функцию.

Ваш код выглядит следующим образом

import React from 'react';

function Feed() {
  const [feeds, setFeeds] = React.useState([]);

  React.useEffect(() => {
    const ctl = new AbortController();

    fetch('https://api.domain.com/feeds')
      .then(res => res.json())
      .then(res => setFeeds(res.data))
      .catch(() => {
        // Log the error
      });

    return cleanup() {
      ctl.abort();
    };
  }, [])

  return (
    <ul>
      {feeds.map(feed => (
        <li>{feed.title}</li>
      ))}
    </ul>
  );
}

Хотя, это действительно простая задача, где вы хотите показать пользователям фид в пользовательском интерфейсе.

Вы запутались в том, как протестировать функцию, потому что вы ”должны” получить данные фида из API, но поскольку это тест, вы не будете обращаться непосредственно к API.

Путешествие

Теперь вы ищете возможные решения в Интернете. Поскольку вы используете React и, конечно же, используете Jest и testing-library (это стандартная настройка от CRA), кто-то предлагает вам поиздеваться над объектом window.fetch.

Вы заинтересовались и вставили решение в свой тест следующим образом

// Import all dependencies... 

const MOCK_FEEDS = [/** Feed objects here */];

beforeAll(() => {
  jest.spyOn(window, 'fetch');
});

describe('Feed Feature Test', () => {
  it('should shows users feed', () => {
    window.fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ data: MOCK_FEEDS }),
    })
    // Your testing goes here... 
  });
});

Вы тестируете код, и он выглядит зеленым (значит, тест пройден), и вы довольны этим.

Наступило завтра. Вы забыли о пользовательском интерфейсе ”пустое состояние” для функции Feed, и сегодня вы его сделали.

Поскольку UI с пустым состоянием должен выдавать ошибку HTTP NOT FOUND, вам нужно переделать тест.

// Import all dependencies... 

const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];

beforeAll(() => {
  jest.spyOn(window, 'fetch');
});

beforeEach(() => {
  jest.restoreAllMocks()
});

describe('Feed Feature Test', () => {
  it('should shows empty state UI when feeds not found', () => {
    window.fetch.mockResolvedValueOnce({
      ok: false,
      status: 404,
      json: async () => ({ data: MOCK_FEEDS_NOT_FOUND }),
    })
    // Your testing goes here... 
  });

  it('should shows users feed', () => {
    window.fetch.mockResolvedValueOnce({
      ok: false,
      json: async () => ({ data: MOCK_FEEDS }),
    })
    // Your testing goes here... 
  });
});

Ваш тест пройден, и вы снова счастливы.

На следующее утро вам позвонил менеджер проекта. Он хочет, чтобы вы добавили кнопку “like (❤️)”, и вы делаете это.

В рамках этой задачи вы должны обновить функцию, чтобы получить две конечные точки API. Сначала вы должны использовать GET /feeds, а затем POST /feeds/{id}/like для выполнения функции “like this feed”.

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

Теперь тест выглядит следующим образом

// Import all dependencies... 

const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];

beforeAll(() => {
  jest.spyOn(window, 'fetch');
});

beforeEach(() => {
  window.fetch.mockImplementation(async (url, config) => {
    switch (url) {
      case '/feeds':
        return {
          ok: true,
          json: async () => MOCK_FEEDS,
        };
      case '/feeds/1/like':
        return {
          ok: true,
          json: async () => ({ liked: true })
        };
      default:
        throw new Error(`Unhandled request: ${url}`);
    }
  });
});

describe('Feed Feature Test', () => {
  it('should shows empty state UI when feeds not found', () => {
    // Your testing goes here... 
  });

  it('should shows users feed', () => {
    // Your testing goes here... 
  });

  it('should able to like the feed', () => {
    // Your testing goes here...
  })
});

Тесты пройдены, кроме первого. Вы спрашиваете ”почему?”, а затем понимаете, что нет обработчика запроса для GET /feeds со статусом 404 http.

Решение

К счастью, существует инструмент под названием Mock Service Worker. Это инструмент для имитации API и работы на сетевом уровне.

Просто представьте, что вы можете подражать вашему API серверу и использовать его внутри вашего теста без головной боли.

И самое интересное:

Поддержка Rest API и GraphQL
Поддержка Node и Browser (то есть вы можете использовать его с Express и т.д.).
И многое другое…

Хватит объяснять, давайте установим требования и отрефакторим тесты.

Во-первых, установите msw.

npm i -D msw

Чтобы использовать MSW, вам нужно сделать обработчики запросов с определенным методом HTTP и URL. Так, он дает вам возможность сделать GET 200 /feeds и GET 404 /feeds. Он также поддерживает параметр URL, такой как /feeds/:feedId/like.

Теперь обновите тест следующим образом…

// Import all dependencies... 
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];

const url = (path) => `https://api.domain.com/feeds/${path}`;

const defaultHandlers = [
  rest.get(url(path), (req, res, ctx) => {
    return res(ctx.json(MOCK_FEEDS));
  }),
  rest.get(url(path), () => {
    return res(
      ctx.status(404),
      ctx.json(MOCK_FEEDS_NOT_FOUND),
    );
  })
];

const server = setupServer(...defaultHandlers);

describe('Feed Feature Test', () => {
  beforeAll(() => {
    server.listen();
  });

  afterEach(() => {
    server.resetHandlers();
  });

  afterAll(() => {
    server.close();
  });

  it('should shows empty state UI when feeds not found', () => {
    // Your testing goes here... 
  });

  it('should shows users feed', () => {
    // Your testing goes here... 
  });

  it('should able to like the feed', () => {
    // Adds the "POST /feeds/:feedId/like" request handler as a part of this test.
    server.use(
      rest.post(url('feeds/:feedId/like'), (req, rest, ctx) => {
        return res(
          ctx.json({ liked: true }),
        );
      }),
    );
    // Your testing goes here...
  })
});

Бум! Ваши тесты пройдены. Не только тесты пройдены, но и улучшен DX, потому что вы можете легко создать мощную маршрутизацию URL.

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

MSW не только обеспечивает лучший DX, но и предлагает вам HTTP Cookie и другие возможности HTTP. Потрясающе!

Заключение

Издевательство (и тестирование) HTTP-запросов - одна из сложных задач, но MSW предлагает мощные возможности, кросс-платформенность и простой подход к решению этой проблемы.

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

Бесшовная библиотека мокинга REST/GraphQL API для браузера и Node.js.

Mock Service Worker (MSW) - это бесшовная библиотека мокинга REST/GraphQL API для браузера и Node.js.

Особенности

Бесшовность. Специальный уровень перехвата запросов в вашем распоряжении. Код и тесты вашего приложения не будут знать о том, высмеивается что-то или нет.

Без отклонений. Запрашивайте те же производственные ресурсы и тестируйте реальное поведение вашего приложения. Дополняйте существующий API или разрабатывайте его на ходу, когда его нет.

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

”Я нашел MSW и был в восторге от того, что я не только мог видеть имитированные ответы в своих DevTools, но и от того, что имитаторы не нужно было писать в Service Worker, а можно было жить рядом с остальными приложениями.