Практический пример с JavaScript и Jest

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

Ситуация

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

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

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

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

Функция

export const copyData = () => {
  if (navigator.clipboard) {
    navigator.clipboard.writeText(JSON.stringify(data.getData()));
  }
};

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

data.getData() — это просто геттер, который извлекает текущее состояние в виде отформатированного объекта JavaScript, соответствующего формату JSON. Для нашего обсуждения здесь мы можем спокойно игнорировать детали реализации этого геттера или состояния, если на то пошло. Все, что нам нужно знать, это то, что геттер возвращает объект.

Как только мы получим этот объект, в коде мы просто сериализуем его как строку, используя метод JSON.stringify. Наконец, мы записываем эти строковые данные в буфер обмена.

Тестовый случай

Я использовал среду тестирования Jest для выполнения модульных тестов.

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

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

test("copies data to the clipboard", () => {
  copyData();
  expect(data.getData).toBeCalledTimes(1);
  expect(navigator.clipboard.writeText).toBeCalledTimes(1);
  expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
    JSON.stringify(mockData)
  );
});

В приведенном выше тестовом примере navigator.clipboard — это зависимость, которую мы хотим имитировать. Кроме того, насмешки необходимы нам, чтобы использовать некоторые вспомогательные методы Jest, такие как toBeCalledTimes и toHaveBeenCalledWith, для выполнения некоторых тестовых утверждений.

Еще один момент, о котором следует помнить, заключается в том, что мы тестируем только наш метод copyData, а тестирование реализации буфера обмена не входит в задачу. На самом деле, тестирование этой зависимости в нашем модульном тесте было бы антишаблоном.

Макет буфера обмена

describe("Application event handlers: copyData method", () => {
  const mockData = {
    title: "my article",
    description: "my test article data",
  };

  const originalClipboard = { ...global.navigator.clipboard };

  beforeEach(() => {
    const mockClipboard = {
      writeText: jest.fn(),
    };
    global.navigator.clipboard = mockClipboard;
    jest.spyOn(data, "getData").mockReturnValue(mockData);
  });

  afterEach(() => {
    jest.resetAllMocks();
    global.navigator.clipboard = originalClipboard;
  });

  test("copies data to the clipboard", () => {
    copyData();
    expect(data.getData).toBeCalledTimes(1);
    expect(navigator.clipboard.writeText).toBeCalledTimes(1);
    expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
      JSON.stringify(mockData)
    );
  });
});

Давайте разберем важные этапы имитации в приведенном выше наборе тестов, потому что в нем используется несколько разных методов имитации:

перед каждым

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

Хук beforeEach запускается перед каждым тест-кейсом, то есть перед каждым хуком test.

Хук beforeAll запускается перед каждым набором тестов, то есть перед каждым хуком describe.

  1. jest.fn() :

Это позволяет нам следить за поведением функции, которая вызывается косвенно из тестируемого кода.

В этом случае метод буфера обмена writeText вызывается внутри метода copyData, который мы тестируем.

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

2. Назначение фиктивного объекта буферу обмена. Второе, что мы делаем в обратном вызове setup, — это назначаем фиктивный объект, который мы создали с фиктивной функцией writeText, интерфейсу объекта буфера обмена навигатора. Таким образом, наш фиктивный метод вызывается внутри, когда мы вызываем наш метод copyData в тестовом примере.

после каждого

afterEach является аналогом beforeEach и используется для обеспечения обратных вызовов разрыва.

afterAll — это разборный аналог beforeAll.

3. jest.resetAllMocks(): это разборка, которую мы делаем, чтобы сбросить состояние макета, созданного jest.fn(), который мы сделали при установке. Но в целом это сбрасывало бы все mock-состояния, которые мы создали в коде.

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

4. Сброс исходного буфера обмена. Наконец, мы просто сбрасываем буфер обмена навигатора до исходного интерфейса объекта буфера обмена.

Дополнительные насмешки

jest.spyOn: Мы используем это в нашем наборе тестов, чтобы имитировать возвращаемое значение, которое мы получаем из состояния приложения. Потому что состояние приложения сериализуется и копируется в буфер обмена. Таким образом, насмешка над возвращаемым значением позволит нам сделать некоторые утверждения об этом.

В этой статье нам не нужно беспокоиться о фактической реализации метода getData, используемого в коде. Нам просто нужно понимать, что это всего лишь геттер для объекта состояния приложения.

Утверждения

Теперь, когда у нас смоделированы все наши юнит-зависимости, мы можем приступить к написанию нашего тестового примера и начать делать утверждения, используя вспомогательные методы Jest:

  1. Данные копируются только один раз:

Мы можем утверждать это с помощью утилиты toBeCalledTimes. В нашем тестовом случае мы используем его, чтобы подтвердить, что и получатель состояния, и метод writeText буфера обмена вызываются только один раз.

Мы можем использовать эту утилиту только потому, что мы имитировали методы writeText и getData в обратном вызове установки.

expect(data.getData).toBeCalledTimes(1);
expect(navigator.clipboard.writeText).toBeCalledTimes(1);

2. Убедитесь, что в буфер обмена скопированы правильные данные:

Мы можем использовать вспомогательную функцию toHaveBeenCalledWith, чтобы утверждать, что наш метод copyData записывает ожидаемые данные в буфер обмена.

expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
  JSON.stringify(mockData)
);

видео

Заключение

Таким образом, мы увидели, как можно имитировать зависимости, используемые внутри функции (модуля), и тестировать модуль изолированно.

Если вам интересно попробовать расширение Chrome, которое мы обсуждали в этой статье, вы можете попробовать его здесь. Я также время от времени публикую видеоконтент на моем Youtube-канале BinaryWares.

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

Поговорим с вами в ближайшее время, через мою следующую статью.

Ваше здоровье :)

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .