Error responses

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:
  1. Go to the test case that needs a different network behavior;
  2. Call server.use() with a request handler for the same request but describe a different mocked response;
  3. 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 until server.resetHandlers() function is called, which in our case is called in the afterEach() hook in vitest.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.