DEV Community

Discussion on: Testing a Generic Fetch Item List hook with Mock Service Worker

Collapse
 
kettanaito profile image
Artem Zakharchenko • Edited

Hey, Guilherme.

There are multiple ways to mock error scenarios with MSW. As the default approach, we recommend explicitly specifying which endpoint must return an error:

it('handles server errors gracefully', () => {
  server.use(
    rest.get('/user', (req, res, ctx) => res(ctx.status(500)))
  )

  // render -> fetch -> assert
})
Enter fullscreen mode Exit fullscreen mode

Testing errorr scenarios is testing edge cases. It's not advised to embed edge-case behaviors into your "happy paths" request handlers.

I understand it may feel verbose but having an evident handler-response override declaration is vastly beneficial in the long run. Generally, prefer explicit behaviors, it's better for debugging and allows you to grasp all the changes the test establishes in a direct way.

In the latest version you can use rest.all() if you wish to make all requests to result in the 500 status code:

it('handles server errors gracefully', () => {
  server.use(
    // Make all requests to result in 500.
    rest.all('*', (req, res, ctx) => res(ctx.status(500)))
  )
})
Enter fullscreen mode Exit fullscreen mode

I wouldn't recommend this particular approach with localStorage because it gives you little idea of what the following line affects, unless you inspect your testing setup:

localStorage.setItem('500', 'true')
Enter fullscreen mode Exit fullscreen mode

If you feel like the observability is a little price to pay, you may use this (in the end, it's your setup!). Also, consider relying in a global variable directly, there's no actual reason to bring in localStorage:

global.shouldRequestsError = true
Enter fullscreen mode Exit fullscreen mode

Rely on window if you wish to reuse this setup logic in the browser as well.

Please let me know if there's a certain difficulty you experience when mocking error responses. It's always a good idea to provide your feedback via GitHub Discussions. Thank you!

Collapse
 
rexebin profile image
Rex

Bringing the expert into the discussion makes a huge difference!

If testing query directly, a separate bad path approach is the best because the test can easily change the url.

But when it’s indirect, say if we are testing a form component which would make a request, we would have to touch the implementation details to manually change the url, which is less ideal.

Assuming we keep the mocked api very simple, a switch for a bad path for all apis may work better. If it is simply and works the same for all endpoints, it could be a known convention for the team. This also resembles the real api closer: the same api point can return good or bad response.

I love the idea of using global instead of local storage. I will make the switch, thank you!

Thread Thread
 
kettanaito profile image
Artem Zakharchenko

But when it’s indirect, say if we are testing a form component which would make a request, we would have to touch the implementation details to manually change the url, which is less ideal.

Well, you're still relying on that implementation detail in your handlers. I do share the notion not to expose too many internals in tests—on that I wholeheartedly agree. Yet observability is also a thing to consider, especially when it comes to the testing setup. I'd still go with explicit handler/override declaration 99% of the time.

This also resembles the real api closer: the same api point can return good or bad response.

Yeah, that is a perfectly valid way to model your request handlers. You can either focus each handler on a particular behavior and utilize overrides via server.use(), or scope handlers per resource, like in a conventional server, and include any conditional behavior within. I'd say it's a personal choice, which comes from the complexity of the mocked API before anything else.

My main concern with using global error mocking is the same as using anything globally: it may be hard to keep track of global setups. This is partially solved by descriptive naming (as in global.shouldRequestsError) but it still introduces an implicit piece of logic that is detached from MSW.

The "ideal" setup, in my opinion, would be to expose internals by reference. That's a cost-free way of exposing internal details.

// endpoints.js
export const getUserDetail = '/v2/user'
Enter fullscreen mode Exit fullscreen mode
// app.js
import { getUserDetail } from './endpoints'

function MyApp() {
  fetch(getUserDetail)
}
Enter fullscreen mode Exit fullscreen mode
// app.test.js
import { getUserDetail } from './endpoints'

it('handles server errors gracefully', () => {
  // Referencing the endpoint is a free way of
  // exposing internals.
  server.use(getUserDetail, (req, res, ctx) => res(ctx.status(500)))
})
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
rexebin profile image
Rex

Thank you! I didn’t know the override, my bad, I should read the docs a lot more.

With the override, I am very happy to ditch the global error mocking. So much clearer and so elegant!

Thread Thread
 
rexebin profile image
Rex

@kettanaito Thanks to your help, I have updated @mockapi/msw removing the global error handlers. I also recommended the use of server.use to override endpoints in the readme. Phew, almost mislead the community.

Thread Thread
 
rexebin profile image
Rex

@kettanaito I also updated the tests and setup here in this post to reflect the best practices.

Collapse
 
cathalmacdonnacha profile image
Cathal Mac Donnacha 🚀

Hey @kettanaito

I love the server.use override in tests, very handy! However, how do you handle this in the browser? In that case you probably would have to use a global override as suggested above.

Thread Thread
 
kettanaito profile image
Artem Zakharchenko

Hey.
The browser has its worker.use() counterpart that behaves identically to server.use(). You can still leverage "happy path" scenarios provided to the setupWorker() call, and then append runtime behavior via worker.use().

Yes, to be able to access the worker in the first place you'd have to expose it as a global variable.

Thread Thread
 
cathalmacdonnacha profile image
Cathal Mac Donnacha 🚀 • Edited

Thanks for the quick reply!

This might be a dumb question but can this then be used in chrome console window?

window.msw.rest.get(`*/cars`, (req, res, ctx) => {
  return res(ctx.status(500), ctx.json({error: 'Oops'}));
});
Enter fullscreen mode Exit fullscreen mode

I just tried but still not seeing the error response, I assume my "happy path" worker which is running when my app loads in dev is overriding it. I basically just want an easy way to simulate error scenario while developing locally.

Thread Thread
 
kettanaito profile image
Artem Zakharchenko

Executing that in the browser console should apply the override. If it's not applied, double-check the correctness of the request path. When mocking on runtime, I find it much more comfortable to bind the handlers overrides to the URL query parameter.

  1. Read the parameter in your mock setup.
  2. Append certain handlers based on the parameter (can go as dynamic here as you wish: passing enums for handlers groups, or names of handler modules).

Since the handlers are loaded on runtime, refreshing that runtime will offload them forever. That's where URL-as-state is a more developer-friendly pattern but it takes some time to set up.

Thread Thread
 
cathalmacdonnacha profile image
Cathal Mac Donnacha 🚀 • Edited

Gotcha. The app I am currently looking at does not have react-router or anything similar implemented, so doesn't update the route or query string in the address bar url as the user clicks around. The app is also hosted on a different domain to the api. Therefore, what do you think about just reading the query string via the window location within the handler itself? Like this:

// Example url which developer would navigate to in the browser:
// http://localhost:3000/car-dashboard?error=true

rest.get(`/cars`, (req, res, ctx) => {
  const searchParams = new URLSearchParams(window.location.search);
  const isError = searchParams.get('error') === 'true';

  if (isError) {
    return res(ctx.status(500), ctx.json({error: 'Oops'}));
  }

   return res(ctx.status(200), ctx.json({results: mockCars}));
});
Enter fullscreen mode Exit fullscreen mode

I'd probably use the same approach to simulate an empty state in the UI:


// Example url which developer would navigate to in the browser:
// http://localhost:3000/car-dashboard?error=false&empty=true

rest.get(`/cars`, (req, res, ctx) => {
  const searchParams = new URLSearchParams(window.location.search);
  const isError = searchParams.get('error') === 'true';
  const isEmpty = searchParams.get('empty') === 'true';

  if (isError) {
    return res(ctx.status(500), ctx.json({error: 'Oops'}));
  }

  if (isEmpty) {
    return res(ctx.status(200), ctx.json({results: []}));
  }

   return res(ctx.status(200), ctx.json({results: mockCars}));
});
Enter fullscreen mode Exit fullscreen mode