Mark Joseph Santos
Mark Joseph Santos

Reputation: 91

How to test a function that calls an API using Jest?

I am new to Node.js unit testing with Jest and still learning. I was wondering what is the proper way of unit testing a function that calls an API? Currently I am using cross-fetch library for API calls. I would like to achieve the unit test for payload validation, 2xx and 5xx API response on API calls.

Here's my code:

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
    })
}

Upvotes: 0

Views: 8686

Answers (2)

jonrsharpe
jonrsharpe

Reputation: 121955

Broadly there are two (not mutually exclusive) ways to unit test* a function like this:

  1. Isolated test, with test doubles replacing the collaborators:

    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" }
        );
      });
    });
    

    This is the "easiest" way to do it, but now you're coupled to the httpFetch interface, which breaks the rule "don't mock what you don't own" - if that library's interface changes at some point, these tests won't tell you that.

  2. Integration test, checking what happens at the transport layer using something like Nock or MSW:

    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();
      });
    });
    

    This takes a bit more setup, but means you are less coupled to the httpFetch interface - you could upgrade that library or switch to a different one, for example, and still be confident things were working.

There are other ways to decouple from the specific library's interface; you could write a facade around it and mock that instead, for example. But you'd still want to know that the right request was being made, and you shouldn't test the facade against a test double of the library for the same reason as before.

You may also have higher-level tests, e.g. E2E tests against the real backend or contract tests against a stub of it; this will impact how you want to balance the number and type of your lower-level tests. Overall these options look something like:

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

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

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

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

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

Remember that the goal (or one of them) is to be confident that the code you're writing works and that if that stops being the case you'll find out promptly in a way that helps you fix it.

* There are lots of ideas around exactly what might comprise a "unit test". Given the principles of speed, independence and parallelisability, the definition I've used in this context is: a test that doesn't actually make a network request.

Upvotes: 2

nishkaush
nishkaush

Reputation: 1548

There are 2 approaches to doing this:

Mock or fake the API call and output fake response (error or otherwise)


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

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

Now you can use the mock in a test like so:

// 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) Make the api call by deliberately passing correct and incorrect payload and matching the expected results

In this case, you will need to know how to make the API call fail or pass.

So perhaps your API fails if payload doesn't contain a certain prop or if the prop is of incorrect type.

This approach is dependent on your payload which you provide to the function.

Upvotes: 2

Related Questions