DEV Community

Rex
Rex

Posted on • Updated on

Testing a Generic Fetch Item List hook with Mock Service Worker

Subject Under Test

A generic fetch item list hook used to fetch data from the API server, designed for a generic item list component template, part of a set of generic CRUD templates/schematics that I use for most of my entities.

Behaviours

  1. it takes a required entityName option to determine which API endpoint to make the HTTP call.

  2. it takes an optional queryKey as the query key of useQuery from React-Query. The query key is used to invalidate the query cache when appropriate to show the latest data for the users.

  3. it takes an optional requestConfig of type AxiosRequestConfig to be passed to the HTTP call. So that components can add params to the HTTP request.

  4. it takes an optional t to translate the error message.

  5. it sends error messages to the Message Context.

Mocks

  1. Mock Service Worker
    The test uses MSW to mock the API calls and respond appropriately for the tests. The mocked API code is as below.

  2. useTranslationForTest.ts is a test double to replace the real localisation solution that requires loading dictionaries from a remote API server.

Common Test Setup

QueryClientProviderForTest.ts is used to test all React-Query hooks.

Code

Notes

  1. TestComponent shows how the SUT is to be used.

  2. the template of TestComponent is designed specifically for the tests. isSuccess and isError ensure that the elements are only shown when the HTTP calls are done, making the tests short and straightforward.

  3. TestComponent uses the hook's params as its props so that the tests can customise most of the options outside of the component.

  4. only unmanaged dependencies are mocked: API server and Localisation(because it requires API calls to get dictionary).

  5. I debated if the SUT is too trivial to test because it is a wrapper of useQuery and contains hardly any logic. I decided to write the tests because the SUT has been used heavily.

Discussion (9)

Collapse
guilima profile image
Guilherme Carvalho Lima

Hey Rex, I really liked how you handle error responses by setting specific status code on localStorage. It would be nice if msw had a proper solution, but this approach is very interesting because avoid jests mocks or implementation on production code.

Collapse
kettanaito profile image
Artem Zakharchenko • Edited on

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 Author

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 Author

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 Author

@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 Author

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

Collapse
rexebin profile image
Rex Author • Edited on

Thank you Guilherme.

It feels like a hack but I think it is not.

It is a very interesting topic to discuss, what msw could have done as a proper solution? If you ask me, I would say it is out of scope for msw. msw provide the infrastructure much like any other API server, it is up to us to return whatever is required.

I think we should not develop the mocked API as if we are developing the real thing, there shouldn't be any complicated logic inside a mocked API. We must take the shortest and simplest route to mimic the responses we expect from a real server. We shouldn't care about how and why the response is returned from the real API.

The tests need to communicate with the mocked API and I think using things like localstorage works is one of the "proper" ways. I could be wrong, @kettanaito what do you think?

Collapse
kettanaito profile image
Artem Zakharchenko • Edited on

Hey, Rex.

I wouldn't recommend using localStorage for global error handling but, in the end, it's your testing setup and you should evaluate what works best for you, taking benefits/trade-offs into consideration.

I've written a more detailed response to Guilherme above, let me know what you think about it.

Thank you once more for this fantastic article!