jefflau / jest-fetch-mock

Jest mock for fetch
MIT License
880 stars 116 forks source link

Question: How to define URL-specific mocks? #247

Open haukehem opened 1 year ago

haukehem commented 1 year ago

Background:
I've created a React project with a Clean Architecture approach and now I want to unit-test the Use-Cases (which call Repositories, which call Data-Sources, so on and so on) against mocked API-data to make sure that the call-and-response-flow works throughout all layers (leaving the UI aside - will be covered in UI- & E2E-Tests).

Problem/Question:
When unit-testing a Use-Cases, there are multiple API-calls involved (for example the first one to refresh an access token and the second one to actually get the desired content). Therefore, I need a way define which responses should be returned for which requests.

So, this is my test case to get available offers from the marketplace:

it("successfully receives available offers", async () => {
      // Arrange
      fetch.mockResponse((req) => {
          if (req.url.endsWith('/connect/authorize')) {
              return Promise.resolve(JSON.stringify({
                  id_token: "",
                  refresh_token: "",
                  access_token: ""
              }));
          } else if (req.url.endsWith("/api/Offers")) {
              return Promise.resolve(JSON.stringify([{
                  id: "1",
                  name: "Test",
                  description: "Test",
                  location: "Goslar"
              }]));
          }
          return Promise.resolve("");
      });
      const getAvailableOffers = new GetAvailableOffers(new OfferRepository(new OfferApiDatasource(new BlueprintOffersApiClient())));

      // Act
      const result = await getAvailableOffers.invoke();

      // Assert
      expect(result.length).toBe(0);
  });

Test-Result:
The result of this is the following error being thrown in the OfferRepository:

Get available offers › successfully receives available offers
    FetchError: invalid json response body at  reason: Unexpected end of JSON input
      at node_modules/node-fetch/lib/index.js:273:32

What should happen: By calling the GetAvailableOffers-Use-Case, the BlueprintOffersAPIClient requests a new valid access token. For this, the first Promise should be received. After that, all available offers should be fetched and the second Promise should be received.

Specific question: How can I specify mocked responses for URLs/individual requests, so that when running my test-case my token-refresh-request receives the /connect/authorize mocked response, but my get-available-offers-request receives the mocked response for /api/Offers?

warrenronsiek commented 1 week ago

I have this exact problem. Very surprised that this use case hasn't come up. Seems like the API encourages you to do this with chaining mockIf and doMockIf but they dont work. I.e. the follow test fails:

test('mock multiple endpoints', async () => {
  const overviewResponse = { data: 'overview data' };
  const summaryResponse = { summary: 'a summary' };

  fetchMock
    .doMockIf(/https:\/\/foo\.com\/api1/, req => {
      return Promise.resolve({
        status: 200,
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(overviewResponse),
      });
    })
    .doMockIf(/https:\/\/foo\.com\/api2/, req => {
      return Promise.resolve({
        status: 200,
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(summaryResponse),
      });
    });

  const overviewResult = await fetch('https://foo.com/api1');
  const overviewJson = await overviewResult.json();
  expect(overviewJson).toEqual(overviewResponse);

  const summaryResult = await fetch('https://foo.com/api2');
  const summaryJson = await summaryResult.json();
  expect(summaryJson).toEqual(summaryResponse);
});

Which is counter intuitive as making this work seems like the whole point of having a chainable mockIf. Will update if I figure it out.