Эта статья является частью серии о написании программ CLI для Node.js и, в частности, о их тестировании путем написания тестов E2E / Integration, а не написания обычного модульного модульного теста. Если вы хотите перейти к окончательному коду реализации, отметьте здесь. Ссылки на другие части перечислены ниже:

При модульном тестировании приложений довольно часто избегают попадания в реальные службы или конечные точки по многим причинам: наиболее очевидная, потому что это должен быть модульный тест, предназначенный для тестирования модулей или кусочки функционала. Но также и потому, что запуск тестов для реального сервиса может поставить под угрозу качество сервиса из-за нагрузки на него из-за дополнительной пропускной способности, несоответствий с реальными данными или даже из-за затрат на обслуживание тестового API, работающего только для тестирования. Обычный обходной путь - развернуть тестовый API с помощью Docker в нашем решении CI / CD, который живет только в течение продолжительности тестирования. Но часто работа над этой настройкой действительно занимает много времени, а в некоторых случаях существует слишком большая взаимозависимость между службами, что превращает эту задачу в довольно сложную задачу. Поэтому модульные тесты с имитацией сервисов, обычно с фиксированными данными, собранными из реальной службы, кажутся единственным разумным выбором. Очевидно, что юнит-тесты должны быть дешевыми и работают быстрее, но это не значит, что мы не хотим, чтобы наши тесты E2E выполнялись быстро. Разве не было бы неплохо, если бы мы могли смоделировать определенные элементы нашей архитектуры для наших интеграционных тестов?

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

  1. Как сказано в сообщении Эрика, насмешка необходима, когда наша стратегия декомпозиции не удалась. Это означает, что мы не можем проводить дальнейшую декомпозицию, потому что мы тестируем нашу интеграцию между модулями CLI и службами. На самом деле он упоминает, что насмешки подходят для интеграционных тестов.
  2. С технической точки зрения, мы не полностью насмехаемся над каким-либо интерфейсом или зависимостью от нашего инструмента CLI, поскольку мы предоставляем механизм для доставки фиксированных / выборочных данных в наше приложение. Мы могли бы возразить, хотя этот механизм на самом деле является имитацией, но он предназначен для обхода фактического инструмента, поэтому мы не захватываем или заменяем части функциональности, а, скорее, предоставляем инструменту средства для сбора данных из другой источник.

Почему мы не можем просто имитировать правильные модули (например, request или http) и напрямую перехватывать вызовы? Помните, что в нашем случае мы находимся в другом (родительском) процессе. Это означает, что дочерний процесс не разделяет состояние и ресурсы со своим родительским, в отличие от модульных тестов, в которых целевой модуль и средство выполнения тестов находятся под одним и тем же процессом Node.js. При тестировании E2E приложение в целом рассматривается как черный ящик, и нам разрешено взаимодействовать только с предоставленным нами общедоступным интерфейсом. Это означает, что доступ к частям приложения на самом деле не является выбором.

Обходной путь

Возвращаясь к моему варианту использования, теоретически я мог бы отправить фиксированные данные в процесс и добавить флаг к модулю, отвечающему за запрос данных, чтобы ответить приспособлением, если оно предоставлено:

// Mocking Data through IPC
// Place this in your entry file
if (process.env.NODE_ENV === 'test') {
  // This can be done better, just for the sake of example
  global.MOCK_DATA = global.MOCK_DATA || [];
  process.on('message', msg => {
    if (msg.mock) {
      global.MOCK_DATA.push({
        // The interface here is similar to mock libraries
        // like nock: send a Regular Expression to test URLs
        // against and if matches, send the mock response
        match: msg.mock
          ? msg.mock instanceof RegExp
          ? msg.mock
          : new RegExp(msg.mock, 'i')
          : /.*/gi,
        method: msg.method,
        response: {
          status: msg.response.status,
          data: msg.response.data
        }
      });
    }
  });
}

Чтобы этот подход работал, вам может потребоваться создать прокси-метод, из которого можно будет перехватывать вызовы API и отправлять данные, если есть какое-либо совпадение в global.MOCK_DATA, аналогично шаблону кеширования:

// request_handler.js
// Replace here with the request library of your choice
const axios = require('axios');
module.exports = async function request(args) {
  if (
    process.env.NODE_ENV === 'test' &&
    Array.isArray(global.MOCK_DATA)
  ) {
    let mockedData;
    global.MOCK_DATA.some(mock => {
      // This match is simplified for example purposes
      // implement it better based on your own needs.
      if (
        args.method === mock.method &&
        mock.match.test(args.url)
      ) {
        mockedData = mock.response;
        
        return true;
      }
    });
      
    if (mockedData) {
      return mockedData;
    }
  }
  // Default to real request if no mock found
  return axios(args);
}

Последней частью будет добавление приспособлений к тесту и закрепление всех свободных концов:

// test_runner.js
const cmd = require('./cmd');
describe('Test my process', () => {
  it('should print the correct output', async () => {
    const promise = cmd.execute('my_process.js');
    const childProcess = promise.attachedProcess;
    
    // Send the response you expect from the service
    childProcess.send({
      mock: /^\/api\/endpoint$/, // Endpoint to match
      method: 'GET', // Target method to mock
      response: { data: 'IPC works!' },
    });
    
    // Remember to wait until the mock is in place
    // to call the CLI command
    childProcess.send('start');
    const response = await promise;
    expect(response).to.equal(
      'Response is: { data: 'IPC works!' }'
    );
  });
});

В заключение

Как мы видели в этой серии статей, написание тестов E2E для приложений CLI не так просто, как кажется, но, если все сделано правильно, это может привести к повышению качества программного обеспечения в целом. Традиционно тесты E2E выполняются инженерами QA Automation, но, учитывая, что большинство инструментов CLI предназначены для облегчения жизни разработчиков, а основными потребителями приложений CLI являются другие разработчики, такого рода тесты часто упускаются из виду. Тем не менее, если приложение CLI наберет достаточную популярность, автоматическое тестирование может облегчить боль при предоставлении новых, обратно совместимых функций для наших инструментов и, в конечном итоге, предоставить надежные приложения.

Мы начали с тестирования простого ввода / вывода для тестирования взаимодействия и асинхронных сервисов с полноценным имитирующим решением. Я не сделал домашнее задание, чтобы проверить, есть ли лучший набор для тестирования E2E на других языках программирования - который, кстати, может лучше подходить для такого рода задач - и, конечно, я призываю всех вас поделиться своим опытом с другими решениями. Расскажите в комментариях, как вам понравилось! Что касается меня, как разработчика JavaScript, я чувствую, что нам не хватает такого рода тестирования наших инструментов, и, черт возьми, мы используем инструменты CLI!

😱 Не могу поверить, что прошел уже почти год с тех пор, как я опубликовал первую часть этой амбициозной серии. Я действительно не ожидал, что это займет у меня так много времени. С тех пор я участвовал в разных проектах, получал опыт и учился на различных инструментах и ​​технологиях, и, конечно же, я также хочу поделиться некоторыми знаниями, которые я накопил за прошедший год. В каком-то смысле я чувствую облегчение, заканчивая эту серию. Как говорится, последняя миля всегда самая длинная. Но, конечно, вы могли случайно наткнуться на этот пост недавно, что тоже неплохо. Я надеюсь, что некоторые идеи, которыми я поделился здесь, помогут вам в вашем решении для тестирования, и, как всегда, не стесняйтесь сообщать мне о любых обнаруженных вами ошибках. Я буду счастлив рассказать о том, как вы разработали решение для тестирования. До скорого! 🎉