Как имитировать запросы на модульное тестирование в Node

kak imitirovat zaprosy na modulnoe testirovanie v node?v=1656609130

Эдо Ривайи

0*FDlur-dky_pPFMag
«Старый кассетный проигрыватель Philips и лента лежат на деревянном полу в Италии» Симоне Аквароли на Unsplash

Скажем, вы решили проверить свою кодовую базу и прочли, что модульные и интеграционные тесты не должны выполнять ввод-вывод. Вы поняли, что вам нужно высмеивать исходящие HTTP-запросы, которые делает ваше приложение, но вы не знаете, с чего начать.

Я решил спросить Кента К. Доддса у Twitter, как он относится к насмешке HTTP:

Честно, Кент! Я считаю, что эта тема заслуживает более подробного описания.

TL; DR

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

  1. Разделите HTTP-запросы из вашей бизнес-логики обработки ответа. Очень часто код, обрабатывающий протокол уровня HTTP, не очень интересен и, возможно, не требует тестирования. Используйте свой инструмент для издевки, чтобы высмеивать свою оболочку API.
  2. Если вам действительно нужно проверить код, специфичный для HTTP, а ответ от внешнего API относительно прост, воспользуйтесь Nock и вручную воспроизводите запросы.
  3. Если ответ, который вам нужно проверить, достаточно сложен, используйте nock-record чтобы записать ответ один раз и использовать эту запись для следующих тестов.

Поскольку сообщество тестирования одержимо пирамидами, вот:

1*w3qPSBXV3ujMUrgT-rIBpQ
HTTP-улыбка пирамида. «API Wrappers + обычная насмешка» в основе. «Ручные ноки» посередине. «Записи Nock» вверху.

Введите Nock

Я бы сказал, что общий консенсус в земле NodeJS заключается в использовании nock работающий путем исправления родного Node http модуль. Это работает очень хорошо, потому что даже если вы не используете http модуль напрямую, как большинство пользовательских библиотек axios, superagent и node-fetch все еще пользуюсь http под капотом.

Написание и использование а Nock выглядит так:

// Set up an interceptornock('  .post('/login', 'username=pgte&password=123456')  .reply(200, { id: '123ABC' });
// Run your code, which sends out a requestfetchUser('pgte', '123456');

В приведенном выше примере fetchUser пришлет запрос POST к example.com/login . Nock перехватит запрос и немедленно ответит вашим предварительно определенным ответом, фактически не касаясь сети. Прекрасно!

Это не так просто

Когда я в первый раз начал работать с Nock, я с нетерпением начал использовать его для своих модульных тестов. Однако я быстро почувствовал, что тратю больше времени на написание Nocks, чем на тестирование бизнес-логики. Одним из решений этого является разделите код запроса от вашей бизнес-логики. Давайте посмотрим на некоторый код.

async function getUser(id) {  const response = await fetch(`/api/users/${id}`);    // User does not exist  if (response.status === 404) return null;
  // Some other error occurred  if (response.status > 400) {    throw new Error(`Unable to fetch user #${id}`);  }    const { firstName, lastName } = await response.json();  return {    firstName,    lastName,    fullName: `${firstName} ${lastName}`  };}

Приведенный выше код посылает запрос на /api/users/<user id>, и когда пользователь найден, он получает объект, содержащийing a firstName and lastName. Наконец, он конструирует объект, имеющий доп field fullName, которое вычисляется из имени и фамилии, полученных по запросу.

Набор тестов для этой функции может выглядеть следующим образом:

it('should properly decorate the fullName', async () => {  nock('    .get('/api/users/123')    .reply(200, { firstName: 'John', lastName: 'Doe });    const user = await getUser(123);  expect(user).toEqual({    firstName: 'John',    lastName: 'Doe,    fullName: 'John Doe'  });});
it('should return null if the user does not exist', async () => {  nock('    .get('/api/users/1337')    .reply(404);    const user = await getUser(1337);  expect(user).toBe(null);});
it('should return null when an error occurs', async () => {  nock('    .get('/api/users/42')    .reply(404);    const userPromise = getUser(42);  expect(userPromise).rejects.toThrow('Unable to fetch user #42');});

Как видите, в этих тестах проходит достаточно много. Давайте разделим функцию на две части:

  • код, который отправляет и обрабатывает запрос HTTP
  • наша бизнес-логика

Наш пример немного надуман, поскольку единственная бизнес-логика, которую мы имеем, это «вычислять». fullName . Но вы можете представить, как реальное приложение будет иметь более сложную бизнес-логику.

// api.jsexport async function getUserFromApi(id) {  const response = await fetch(`/api/users/${id}`);    // User does not exist  if (response.status === 404) return null;
  // Some other error occurred  if (response.status > 400) {    throw new Error(`Unable to fetch user #${id}`);  }
  return response.json();}
// user.jsimport { getUserFromApi } from './api';
async function getUserWithFullName(id) {  const user = await getUserFromApi(id);  if (!user) return user;
  const { firstName, lastName } = user;  return {    firstName,    lastName,    fullName: `${firstName} ${lastName}`  };}

Чтобы не надоедать вам до смерти, я покажу вам только тесты на нашу бизнес-логику. Вместо того, чтобы использовать Nock для насмешки HTTP-запроса, теперь вы можете использовать выбранную библиотеку для насмешки, чтобы имитировать нашу собственную оболочку API. Я предпочитаю Jest, но этот шаблон не привязан к какой-либо конкретной библиотеке насмешки.

// The function we're testingimport { getUserWithFullName } from './user';
// Only imported for mockingimport { getUserFromApi } from './api';
jest.mock('./api');
it('should properly decorate the fullName', async () => {  getUserFromApi.mockResolvedValueOnce(    { firstName: 'John', lastName: 'Doe }  );    const user = await getUserWithFullName(123);  expect(user).toEqual({    firstName: 'John',    lastName: 'Doe,    fullName: 'John Doe'  });});
it('should return null if the user does not exist', async () => {  getUserFromApi.mockResolvedValueOnce(null);    const user = await getUserWithFullName(1337);  expect(user).toBe(null);});

Как видите, наши тесты выглядят несколько чище. Все затраты HTTP теперь содержатся в модуле API. Что мы фактически сделали, это минимизировали поверхность нашего кода, которая знает о транспорте HTTP. Делая это, мы минимизируем необходимость использования Nock в наших тестах.

Но логика HTTP – это то, что я хочу проверить!

Я тебя слышу. Иногда подключение к внешнему API – именно то, что вы хотите проверить.

Я уже показал, как вы можете использовать Nock для насмешки очень простого HTTP-запроса. Написание явных Nocks для таких простых пар запрос/ответ очень эффективен, и я бы рекомендовал придерживаться этого как можно больше.

Однако иногда содержание запроса или ответа может быть достаточно сложным. Написание ручных Nocks для таких случаев быстро становится утомительным, а также хрупким!

Очень ярким примером такого случая может являться испытание скребка. Основная ответственность скрепера – превратить необработанный HTML в полезные данные. Однако при тестировании скрепера вы не хотите вручную создавать HTML-страницу для подачи в Nock. Более того, сайт, который вы собираетесь сцепить, уже имеет HTML, который вы хотите обработать, так что давайте воспользуемся этим! Think Jest Snapshots, для насмешки HTTP.

Удаление тем из Medium

Скажем, я хочу знать все темы, доступные на Medium.

1*GTktvsL1PGGUyaHfpObi6w
Снимок экрана домашней страницы media.com, показывающий список доступных тем

Мы воспользуемся scrape-it чтобы запросить домашнюю страницу Medium и извлечь тексты из всех соответствующих элементов .ds-nav-item :

import scrapeIt from "scrape-it";
export function getTopics() {  return scrapeIt(" {    topics: {      listItem: ".ds-nav-item"    }  }).then(({ data }) => data.topics);}
// UsagegetTopics().then(console.log);// [ 'Home', 'Tech', 'Culture', 'Entrepreneurship', 'Self', 'Politics', 'Media', 'Design', 'Science', 'Work', 'Popular', 'More' ]

? Выглядит хорошо!

Теперь как мы будем высмеивать фактический запрос в нашем тесте? Один из способов этого – перейти на medium.com в нашем браузере, просмотреть источник и скопировать/вставить это в Nock вручную. Это утомительно и склонно к ошибкам. Если мы действительно хотим получить весь HTML-документ, мы можем позволить компьютеру справиться с этим за нас.

Оказывается, у Nock есть интегрированный механизм под заглавием «Запись». Это позволит вам использовать перехватчики Nock для перехвата фактического HTTP-трафика, а затем сохранить пару запрос/ответ в файле и использовать это запись для будущих запросов.

Лично я считаю функциональность записей Nock очень полезна, но эргономику можно улучшить. Так вот моя беззастенчивая пробка для nock-record более эргономичная библиотека для использования записей:

0*jojs7J_uR9k56M3C
Скринкаст нок-рекорда в действии. Показывая, как при начальном тестовом запуске отправляются фактические запросы HTTP, а следующие запуски будут использовать записи первого запуска, чтобы предотвратить будущие запросы.

Давайте посмотрим, как мы можем проверить наш скребок с помощью nock-record :

import { setupRecorder } from 'nock-record';import { getTopics } from './index';
const record = setupRecorder();
describe('#getTopics', () => {  it('should get all topics', async () => {    // Start recording, specify fixture name    const { completeRecording } = await record('medium-topics');
    // Our actual function under test    const result = await getTopics();        // Complete the recording, allow for Nock to write fixtures    completeRecording();    expect(result).toEqual([      'Home',      'Tech',      'Culture',      'Entrepreneurship',      'Self',      'Politics',      'Media',      'Design',      'Science',      'Work',      'Popular',      'More'    ]);  });});

Когда мы впервые запустим этот тест, он отправит фактический запрос на получение HTML домашней страницы Medium:

✓ should get all topics (1163ms)

После этого первого запуска, nock-record сохранил запись в файл по адресу
__nock-fixtures__/medium-topics.json . Для второго запуска, nock-record автоматически загрузит запись и настроит для вас Nock.

✓ should get all topics (116ms)

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

Теперь мы получили 3 вещи, используя записи:

  1. Детерминирован: ваш тест будет всегда выполняться с одним и тем же HTML-документом
  2. Быстро: следующие тесты не попадут в сеть
  3. Эргономика: не нужно вручную жонглировать приборами для реакции

Дайте мне знать, что вы думаете

Подход, который я изложил в этой статье, хорошо сработал для меня. Я хотел бы услышать ваш опыт в комментариях или в твиттере: @EdoRivai .

То же касается nock-record; вопросы и PR приветствуются!

Добавить комментарий

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