Error responses
Loading "Error Responses (๐ solution)"
Run locally for transcripts
I start by restructuring our project a little bit. In order to use runtime handlers, I need to have access to the same
server
object created in vitest.setup.ts
. However, importing from the setup file is not recommended.๐ฆ Although Vitest will cache imports and won't evaluate the global hooks twice if you import from the setup file, it caches imports per test file. This works nicely in the isolated mode, which Vitest uses by default, but may cause problems if you switch to parallel test runs.
The directory structure should now look like this:
/
/mocks
handlers.ts # Request handlers for happy paths.
node.ts # Node.js integration point.
get-auth-token.test.ts # import { server } from '...'
vitest.setup.ts # import { server } from '...'
With these changes, I can reuse the same
server
object from ./mocks/node.ts
in both vitest.setup.ts
and in the test suite as well.import { server } from './mocks/node.js'
beforeAll(() => {
server.listen({
onUnhandledRequest: 'error',
})
})
afterEach(() => {
server.resetHandlers()
})
afterAll(() => {
server.close()
})
import { http } from 'msw'
import { server } from './mocks/node.js'
import { getAuthToken } from './get-auth-token.js'
At last, I can prepend request handlers using the
server.use()
API to model network scenarios on a per-test basis! The workflow for overriding the network will be the same no matter where I use it:- Go to the test case that needs a different network behavior;
- Call
server.use()
with a request handler for the same request but describe a different mocked response; - Execute the tested code (the code that makes the request).
Here's what the invalid credentials scenario will look like with the runtime handler in place:
test('throws an error if the user credentials are invalid', async () => {
server.use(
http.post('https://api.example.com/auth', () => {
return new Response(null, { status: 401 })
}),
)
await expect(() =>
getAuthToken({
email: 'kody@epicweb.dev',
password: 'supersecret123',
}),
).rejects.toThrow('Authentication failed: invalid credentials')
})
server.use()
prepends request handlers so they take higher priority. The prepended handlers will persist untilserver.resetHandlers()
function is called, which in our case is called in theafterEach()
hook invitest.setup.ts
.
In a similar fashion, I will create a runtime request handler to emulate a generic 500 server error response in another test case:
test('throws an error if the server responds with an error', async () => {
server.use(
http.post('https://api.example.com/auth', () => {
return new Response(null, { status: 500 })
}),
)
Lastly, I will run the tests to make sure everything works as expected:
โ get-auth-token.test.ts (3)
โ returns the authentication token on successful authentication
โ throws an error if the user credentials are invalid
โ throws an error if the server responds with an error
Further reading
Runtime request handlers are a powerful way to describe dynamic network behaviors. I highly recommend you to read the ๐ Network behavior overrides best practice guide to learn more how you can use them when developing and testing your projects.