jefflau / jest-fetch-mock

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

Confused about conditional mocking (doMockIf, doMockOnceIf) #145

Closed pastinepolenta closed 3 years ago

pastinepolenta commented 4 years ago

I thought the need of mocking specific requests with specific responses and leave the other to the default behavior is a pretty common need and it was expressed by https://github.com/jefflau/jest-fetch-mock/issues/95 and I though it was solved by https://github.com/jefflau/jest-fetch-mock/pull/128 introducing doMockIf and doMockOnceIf.

I still fail to understand though how to properly mock 3 API calls no matter the order while leaving the other mocked by default.

I tried a chain like

fetch
   .doMockOnceIf(/islogged/, true)
   .doMockOnceIf(/token/, 'randomtoken')
   .doMockOnceIf(/info/, infoStub)

But that seems to mock properly only if the order of the calls is known and the calls are executed in sequence.

Am I missing something?

yinzara commented 4 years ago

The "Mock*If" functions determine if it mocks or not, not the content of the response. You're just looking for a single call to mockResponse.

fetch.mockResponse(req => {
    if (req.url.endsWith("/isLogged")) {
        return "true" // must be a string response
    } else if (req.url.endsWith("/token")) {
       return (Math.random() * 100000).toString()
    } else if (req.url.endsWith("/info")) {
        return infoStub
    } else {
        return "default mock response"
    }
})

If you wanted to say "if the URL matches any of my valid 3 cases, mock the response as specified, otherwise, use the real fetch implementation" you would do:

fetch.doMockIf(/^.*(\/isLogged|\/token|\/info)$/, req => {
    if (req.url.endsWith("/isLogged")) {
        return "true" // must be a string response
    } else if (req.url.endsWith("/token")) {
       return (Math.random() * 100000).toString()
    } else if (req.url.endsWith("/info")) {
        return infoStub
    } else {
        // should never happen
        return ""
    }
})
milotoor commented 4 years ago

@yinzara Thanks for the help! I've noticed that your first example trips up the Typescript compiler:

fetchMock.mockResponse((req) => {
  if (req.url.match(/urlPattern1/)) {
    return JSON.stringify(response1);
  } else if (req.url.match(/urlPattern2/)) {
    return JSON.stringify(response2);
  } else {
    return "default mock response";
  }
});

Results in:

TS2769: No overload matches this call. 
  Overload 1 of 2, '(fn: MockResponseInitFunction): FetchMock', gave the following error.
    Argument of type '(req: Request) => string' is not assignable to parameter of type 'MockResponseInitFunction'.
      Type 'string' is not assignable to type 'Promise<string | MockResponseInit>'.
  Overload 2 of 2, '(response: string, responseInit?: MockParams | undefined): FetchMock', gave the following error.
    Argument of type '(req: Request) => string' is not assignable to parameter of type 'string'.

Inspecting the mockResponse type definition:

mockResponse(fn: MockResponseInitFunction): FetchMock;
mockResponse(response: string, responseInit?: MockParams): FetchMock;

And MockResponseInitFunction looks like:

export type MockResponseInitFunction = (request: Request) => Promise<MockResponseInit | string>;

So it's looking for a Promise. In short, changing my initial code sample to pass an async function resolves the issue, but there really isn't any need for the function to be async:

fetchMock.mockResponse(async (req) => {
  if (req.url.match(/urlPattern1/)) {
    return JSON.stringify(response1);
  } else if (req.url.match(/urlPattern2/)) {
    return JSON.stringify(response2);
  } else {
    return "default mock response";
  }
});

Would it be possible to modify the type declaration (and the implementation) to simply accept a string response? I'm happy to put together a PR.

gannonprudhomme commented 3 years ago

Would love this added! The No overload matches this call. error for the examples were tripping me up in my Typescript project for a bit too long.