Как протестировать функцию, которая вызывает API с помощью Jest?

#node.js #unit-testing #jestjs

#node.js #модульное тестирование #jestjs

Вопрос:

Я новичок в Node.js модульное тестирование с помощью Jest и все еще обучение. Мне было интересно, каков правильный способ модульного тестирования функции, которая вызывает API? В настоящее время я использую библиотеку перекрестной выборки для вызовов API. Я хотел бы выполнить модульный тест для проверки полезной нагрузки, ответа API 2xx и 5xx на вызовы API.

Вот мой код:

 export const myFunction = (payload: any) => {
  if (_.isNull(payload) || _.isUndefined(payload)) {
    throw new Error('payload is required')
  }

  httpFetch('http://localhost/api/send', { method: 'POST' }, { 'content-type': 'application/json', Authorization: 'Bearer 12ABC'})
    .then((resp) => {
      // ...return 2xx
    })
    .catch((e) => {
       // ...return 5xx
    })
}
  

Комментарии:

1. Ответственность этой функции заключается в вызове httpFetch , поэтому вы можете заменить ее на тестовый double (хотя обратите внимание, что ваша функция на самом деле ничего не возвращает …) и проверить, что она вызывается с правильными вещами (по-видимому, не включая полезную нагрузку). В качестве альтернативы вы можете выполнить больше интеграционных тестов, используя что-то вроде nock проверки правильности выполнения запроса.

2. Спасибо за ответ @jonrsharpe, можете ли вы предоставить образец?

3. Примечание myFunction , вероятно, не следует возвращать 2xx или 5xx — это деталь транспортного уровня, вы же не хотите, чтобы все ваше приложение полагалось на это.

Ответ №1:

Для этого есть 2 подхода:

Имитируйте или подделывайте вызов API и выводите поддельный ответ (ошибка или иное)

 
httpFetch = jest.fn(()=>Promise.resolve("provide-dummy-response-payload"));

httpFetch = jest.fn(()=>Promise.reject("provide-dummy-error-payload"));

  

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

 // pseudo code

  it("makes the api call successfully",async ()=>{
     httpFetch = jest.fn(()=>Promise.resolve("provide-dummy-response-payload"));
     const result = await myFunction("random-payload");
     // make assertions about the result here
  });

  it("fails the api call",async ()=>{
     httpFetch = jest.fn(()=>Promise.reject("provide-dummy-error-payload"));
     const error = await myFunction("random-payload");
     // make assertions about error here
  });

  

(2) Выполните вызов api, намеренно передавая правильную и неправильную полезную нагрузку и сопоставляя ожидаемые результаты

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

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

Этот подход зависит от вашей полезной нагрузки, которую вы предоставляете функции.

Ответ №2:

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

  1. Изолированный тест с двойными тестами, заменяющими соавторов:

     import httpFetch from "wherever";
    
    import myFunction from "somewhere";
    
    jest.mock("wherever");
    
    describe("myFunction", () => {
      it("calls httpFetch", async () => {
        httpFetch.mockResolvedValue();
    
        await myFunction({});
    
        expect(httpFetch).toHaveBeenCalledWith(
          "http://localhost/api/send",
          { method: "POST" },
          { "Content-Type": "application/json", Authorization: "Bearer 12ABC" }
        );
      });
    });
      

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

  2. Интеграционный тест, проверяющий, что происходит на транспортном уровне, используя что-то вроде Nock:

     import nock from "nock";
    
    import myFunction from "somewhere";
    
    describe("myFunction", async () => {
      it("makes the right request", () => {
        const scope = nock("http://localhost/api", {
          reqheaders: {
            "Content-Type": "application/json",
            Authorization: "Bearer 12ABC",
          },
        })
          .post("/send")
          .reply(201);
    
        await myFunction({});
    
        scope.done();
      });
    });
      

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

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

У вас также могут быть тесты более высокого уровня, например, тесты E2E против реального серверного сервера или контрактные тесты против его заглушки; это повлияет на то, как вы хотите сбалансировать количество и тип ваших тестов более низкого уровня. В целом эти параметры выглядят примерно так:

 System:      [Service] -> [Library] -> [HTTP] -> [Backend]

Isolated:    |<----->| -> (Test Double)

Integration: |<------------------>| -> (Nock)

Contract:    |<---------------------------->| -> (Stub)

E2E:         |<----------------------------------------->|

  

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

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