DOM API, частичные имитации и развлечения

В последнее время я работал над добавлением дополнительных модульных тестов в интерфейсный код. Использование замечательной библиотеки тестирования реакции Кента С. Доддса (если вы еще не проверили ее, серьезно 🔥) вместе с Jest. Тестировать небольшие компоненты обычно довольно просто. Передайте им смоделированные функции в качестве реквизита, активируйте некоторые события и убедитесь, что все выглядит так, как вы expect. Больше всего я забавлялся, когда мне нужно было имитировать зависимости.

Краткий призыв к написанию модульных тестов, таких как производственный код

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

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

Мокинг DOM API

Иногда API-интерфейсы DOM недоступны в JSDOM. Jest называет это в своей документации, рекомендуя просто написать фиктивную функцию и назначить ее объекту окна. Но что, если вы хотите поиздеваться над недвижимостью в окне?

Оказывается, есть простой способ VanillaJS ™ сделать это. Используя Object.defineProperty с объектом окна (или объектом документа), мы можем имитировать геттеры и сеттеры. Вот пример, который я недавно написал для издевательства над document.cookie. Цель состояла в том, чтобы протестировать ситуацию, когда doucment.cookie не может быть установлен из-за ограничений iframe для разных источников в Safari (🙃).

test('Renders help message when cookies cannot be set', () => {
  Object.defineProperty(document, 'cookie', {
    get: jest.fn().mockImplementation(() => { return ''; }),
    set: jest.fn().mockImplementation(() => {}),
  });
  // more test code...
})

Теперь тест делает то, что я хочу. Компонент не сможет установить cookie и должен вести себя правильно.

Альтернативный подход: служебные функции DOM

Другой способ сделать это - вместо того, чтобы напрямую издеваться над document.cookie, мы могли бы написать служебную функцию для компонента, который ее использует. function canSetCookie() -> boolean

Почему это полезно? Потому что нам совсем не нужно высмеивать свойство document. Вместо этого мы можем просто имитировать наш служебный метод, как будто мы имитируем что-нибудь еще в тесте. Например, если функция canSetCookie используется как опора, у нас есть простой способ протестировать этот компонент изолированно.

Или, если он находится в каком-то общем локальном служебном файле, мы можем имитировать эту зависимость. Наряду с кодом cookie у нас также был код, который определял, находимся ли мы в iframe. Поскольку это было во вспомогательном методе, я мог сделать «нормальный» макет со следующим кодом.

jest.mock('shared/util/iframe.js', () => ({
  isInIframe: () => true
}));

Легко, дальше.

Частично имитация зависимостей

Допустим, у вас есть зависимость, в которой вы хотите по-разному имитировать функцию для каждого теста. Например, ситуация, в которой мне нужно это сделать, - это клиентский код. У нас есть клиенты API, которые могут пройти тест по многим маршрутам. Для многих тестов мы хотим вернуть одно и то же из наших моков, но иногда мы можем захотеть протестировать разные сценарии (например, API, возвращающий ответы об ошибках). Вопрос в том, как у нас есть общий макет, а затем время от времени переопределять отдельные экспорты из него? Или, может быть, вы хотите понаблюдать, с чем вызывается фиктивная функция?

Ответ здесь тоже оказывается просто javascript. Шаги следующие:

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

Например:

jest.mock('shared/my-api-client.js');
import { client } from 'shared/my-api-client.js';
test('returns error from client' () => {
  const mockMethod = jest.fn(() => Promise.resolve({}));
  client.sendInfo = mockMethod;
  // test code...
  expect(mockMethod).toHaveBeenCalledWith(expectedJson);
})

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

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