#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:
В целом существует два (не взаимоисключающих) способа модульного тестирования * функции, подобной этой:
-
Изолированный тест с двойными тестами, заменяющими соавторов:
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
интерфейсу, что нарушает правило «не издевайтесь над тем, что вам не принадлежит» — если интерфейс этой библиотеки в какой-то момент изменится, эти тесты вам этого не скажут. -
Интеграционный тест, проверяющий, что происходит на транспортном уровне, используя что-то вроде 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: |<----------------------------------------->|
Помните, что цель (или одна из них) — быть уверенным, что код, который вы пишете, работает, и что, если это перестанет быть так, вы быстро узнаете об этом и сможете исправить это.
* Есть много идей о том, что именно может включать в себя «модульный тест». Учитывая принципы скорости, независимости и параллелизации, определение, которое я использовал в этом контексте, таково: тест, который фактически не выполняет сетевой запрос.