New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add the fetch-mock-jest library to replace the manual mocking of fetch #4020
Conversation
Codecov Report
@@ Coverage Diff @@
## main #4020 +/- ##
==========================================
- Coverage 87.29% 87.29% -0.01%
==========================================
Files 279 278 -1
Lines 24088 24078 -10
Branches 6374 6377 +3
==========================================
- Hits 21027 21018 -9
+ Misses 2820 2819 -1
Partials 241 241
Continue to review full report at Codecov.
|
beforeEach(function () { | ||
// Install fetch and fetch-related objects globally. | ||
// Using the sandbox ensures that parallel tests run properly. | ||
global.fetch = fetchMock.sandbox(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't strictly the recommended way to setup the mock, but the examples are either "in node" or "in browsers", and we're in-between because we're "in node in a jsdom environment".
I initially tried using the cross-fetch
library that installs node-fetch
as a global directly, and try to apply the mock from here, but couldn't make it work properly. Maybe I didn't try it properly.
Anyway this seems to work fine so I think we can revisit later.
Especially when moving to node v18 and fetch
will be available globally in node, we might need to revisit the approach.
// Do the same with fetch mocks | ||
fetchMock.mockReset(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is strictly needed because we add a new version of the sandbox for each test. But better be safe than sorry :)
Also I'm not deleting the fetch
property from the global. This seems to work fine for now but we might need to adjust in the future.
@@ -210,8 +188,8 @@ describe('AppLocalizationProvider', () => { | |||
|
|||
expect(await screen.findByText(translatedText('de'))).toBeInTheDocument(); | |||
expect(document.documentElement).toHaveAttribute('lang', 'de'); | |||
expect(window.fetch).toBeCalledWith('/locales/de/app.ftl'); | |||
expect(window.fetch).toBeCalledWith('/locales/en-US/app.ftl'); | |||
expect(window.fetch).toBeCalledWith('/locales/de/app.ftl', undefined); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like fetch
is always called with 2 arguments now, even if we don't specify the second argument in code, because of how mock-fetch-jest
works (it installs another proxy on top of the mock). Therefore we need to match it too in the jest matcher.
}); | ||
(window: any).fetch = fetch; | ||
window.fetch | ||
.catch(404) // catchall |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default for catch
is to return an empty 200 response. That's why I specify it everywhere,
However whenever a request goes to the catch
there's a warning to the console (this can be disabled). So I tried to specify the rules for every tests so that catch
isn't even reached.
While changing this I found the warning useful, that's why I decided to keep it. But we could possibly configure a global catch
that would throw instead of returning 200.
statusText: 'Method not allowed', | ||
window.fetch | ||
.catch(404) // Catchall | ||
.mock(endpointUrl, (urlString, options) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is mostly similar to the previous code, due to wheresrhys/fetch-mock#636
(window: any).fetch = fetch; | ||
window.fetch | ||
.catch(404) // catchall | ||
.get(url, profile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
much smaller 😁
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely! ✨
"arrayBuffer": [Function], | ||
"headers": Object { | ||
"get": [Function], | ||
Response { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Reponse object is different so these snapshots reflect that change
.get(expectedUrl, makeProfileSerializable(_getSimpleProfile()), { | ||
overwriteRoutes: false, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See wheresrhys/fetch-mock#637 for the background on using { overwriteRoutes }
.
I don't find this very intuitive to be honest. We can also make overwriteRoutes: false
the default in the setup. I didn't do it yet but if we find the behavior is better for us this is possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, this looks interesting. Thanks for filing an issue.
window.fetch.get( | ||
expectedUrl, | ||
compress(serializeProfile(_getSimpleProfile())), | ||
{ sendAsJson: false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sendAsJson: true
is the default behavior. This means that if we send something that's not a normal object (like array buffers here), we need to specify sendAsJson: false
. We might want to do the opposite: set sendAsJson
false by default, but set it to true
when needed. This might be more intuitive. We can also do it in a file setup so that it's global to a test file only.
I decided to wait a bit before changing the default, to see which one would be better.
Note: that's also why you see I replaced encode(serializeProfile(...))
with makeProfileSerializable
in a lot of places, to let the library to the JSON converting. But maybe it would be better in this file to always do the conversion ourselves. I guess we can wait and see.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm yeah, I don't feel strongly about either solution. Waiting and seeing works for me.
isJSON?: true, | ||
arrayBuffer?: () => Promise<Uint8Array>, | ||
json?: () => Promise<mixed>, | ||
content: 'generated-zip' | 'generated-json' | Uint8Array, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: a bigger change, to better account that isZipped
/ isJSON
/ arrayBuffer
were mutually exclusive. Also json
wasn't used.
statusText: 'Method not allowed', | ||
}); | ||
} | ||
window.fetch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly to the test for ListOfPublishedProfiles.test.js
this isn't wildly different than what it was before.
} | ||
|
||
const payload = JSON.parse(body); | ||
return responseFromRequestPayload(payload); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use the functionality to have a response depending on the request body, but I decided to keep it still.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! It looks like it the new mock makes things a lot easier and shorter 🎉
(window: any).fetch = fetch; | ||
window.fetch | ||
.catch(404) // catchall | ||
.get(url, profile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely! ✨
@@ -1,55 +0,0 @@ | |||
/* This Source Code Form is subject to the terms of the Mozilla Public |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
beforeEach(function () { | ||
window.TextDecoder = TextDecoder; | ||
window.fetch = jest.fn().mockResolvedValue(fetch403Response); | ||
window.fetch.catch(403); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really easier to follow with the next get
calls and everything!
.get(expectedUrl, makeProfileSerializable(_getSimpleProfile()), { | ||
overwriteRoutes: false, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, this looks interesting. Thanks for filing an issue.
@@ -1069,7 +1050,7 @@ describe('actions/receive-profile', function () { | |||
|
|||
it('fails in case the fetch returns a server error', async function () { | |||
const hash = 'c5e53f9ab6aecef926d4be68c84f2de550e2ac2f'; | |||
window.fetch = jest.fn().mockResolvedValue(fetch500Response); | |||
window.fetch.any(500); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a while why we needed to use any
here (because we have another .catch
call in beforeEach
block. I was going to recommend using catch but then realized this one. Maybe it could be good to add a comment that explains this here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fundamental difference between catch
and any
is that when catch
is reached a warning will be emitted (which is configurable). Ideally I'd like to avoid that, so ideally it would be good to have routes for every request that's supposed to happen in a test. I think that in the future I'd rather add a .catch({ throws: new Error('No route has been defined for this request') })
in setup.js...
any
is different: this is an expected request (even if it catches everything). I guess I was a bit lazy at the end of the day, and could have used a get
with the full expected hash... I'm changing this :-)
window.fetch.get( | ||
expectedUrl, | ||
compress(serializeProfile(_getSimpleProfile())), | ||
{ sendAsJson: false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm yeah, I don't feel strongly about either solution. Waiting and seeing works for me.
Fixes #3257
This mock mocks the
fetch
command.fetch
can be asserted like a jest mock function (it is a mock function) but its behavior can be configured in a more expressive way.I considered also using
nock
like we do in the server, but settled on that library for now for this reason mainly.The library also comes with custom jest assertion matchers, but I didn't add those to the jest library definition yet, because I didn't use them here. Actually I found that jest's usual matchers work fine already.
Most test files are much simpler, some of them are similar.
Be careful as the changes for
receive-profile.test.js
is hidden by default by github.