Test React component that fetching data
Tags: react
Published: 2023-03-04 14:18:59

## TL;DR

We are going to use [Mock Service Worker](https://mswjs.io) to fake the API. So, instead directly calling the API or mock the `window.fetch` it's better to mock the server behaviour.

It gives us a sandbox to play with the API server without polluting the `window.fetch` and no need to setup test API server for testing purpose.

Thanks to [Artem Zakharchenko](https://github.com/kettanaito) that has made a useful tools.

## Introduction

You have created a beautiful `Feed` feature on your site and now you want to test that feature.

Your code is look like this

```jsx
import React from 'react';

function Feed() {
  const [feeds, setFeeds] = React.useState([]);

  React.useEffect(() => {
    const ctl = new AbortController();
 
    fetch('https://api.domain.com/feeds')
      .then(res => res.json())
      .then(res => setFeeds(res.data))
      .catch(() => {
        // Log the error
      });

    return cleanup() {
      ctl.abort();
    };
  }, [])

  return (
    <ul>
      {feeds.map(feed => (
        <li>{feed.title}</li>
      ))}
    </ul>
  );
}
```

Though, it's really simple frature where you want to show users feed to the UI.

Somehow you confused about how to test the feature because you "must" fetch the feeds data from the API, but since this is a test you won't it calls directly to the API.

## The Journey

Now you are searching possible solutions on the Internet. Because you use React and of course use Jest and testing-library (it is default setup from CRA), someone suggest you to mock the `window.fetch` object.

You intersted and put the solution in your test like this

```jsx
// Import all dependencies... 

const MOCK_FEEDS = [/** Feed objects here */];

beforeAll(() => {
  jest.spyOn(window, 'fetch');
});

describe('Feed Feature Test', () => {
  it('should shows users feed', () => {
    window.fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ data: MOCK_FEEDS }),
    })
    // Your testing goes here... 
  });
});
```

You test the code and it looks green (meaning your test is pass) and you happy with that.

---

Tomorrow has come. You forgot about the "empty state" UI for Feed feature and today you made it.

Because the empty state UI need en error [HTTP NOT FOUND](https://id.wikipedia.org/wiki/HTTP_404), you need to refactor the test.

```jsx
// Import all dependencies... 

const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];

beforeAll(() => {
  jest.spyOn(window, 'fetch');
});

beforeEach(() => {
  jest.restoreAllMocks()
});

describe('Feed Feature Test', () => {
  it('should shows empty state UI when feeds not found', () => {
    window.fetch.mockResolvedValueOnce({
      ok: false,
      status: 404,
      json: async () => ({ data: MOCK_FEEDS_NOT_FOUND }),
    })
    // Your testing goes here... 
  });

  it('should shows users feed', () => {
    window.fetch.mockResolvedValueOnce({
      ok: false,
      json: async () => ({ data: MOCK_FEEDS }),
    })
    // Your testing goes here... 
  });
});
```

Your test is passes, and you happy again.

---

The next morning, you received calls from project manager. He want you to add a "like (❤️)" button and you do it.

From this task, you have to update the feature to fetch two API endpoints. First you must use the `GET /feeds` and then `POST /feeds/{id}/like` to perform "like this feed".

After adding some codes, you refactor the test again, but realise that the mocked fetch doesn't care of the URL. What ever API endpoint you hit, it always returns the same response and potential a bugs on the future.

The test now is look like this

```jsx
// Import all dependencies... 

const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];

beforeAll(() => {
  jest.spyOn(window, 'fetch');
});

beforeEach(() => {
  window.fetch.mockImplementation(async (url, config) => {
    switch (url) {
      case '/feeds':
        return {
          ok: true,
          json: async () => MOCK_FEEDS,
        };
      case '/feeds/1/like':
        return {
          ok: true,
          json: async () => ({ liked: true })
        };
      default:
        throw new Error(`Unhandled request: ${url}`);
    }
  });
});

describe('Feed Feature Test', () => {
  it('should shows empty state UI when feeds not found', () => {
    // Your testing goes here... 
  });

  it('should shows users feed', () => {
    // Your testing goes here... 
  });

  it('should able to like the feed', () => {
    // Your testing goes here...
  })
});
```

The tests are passes except the first one. You asking "why?" then realise there is no request handler for `GET /feeds` with `404` http status.

![Debugging be like](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wdpxevkdpzpaecnt1etr.png)
*https://id.pinterest.com/pin/784259722590566564/*

## The Solution

Luckly, there is a tool called [Mock Service Worker](https://mswjs.io). It is a mocking tool for API and live in the network level.

Simply imagine that you can mock your API server and use it inside your test without makes you headache.

And the interesting part of this are:

- Support Rest API and GraphQL
- Support Node and Browser (meaning you can use it with Express, etc)
- And many more...

Enough for explaining, let's install the requirement and refactor the tests.

---

First, install the `msw`.

```bash
npm i -D msw
```

To use the MSW, you need to make request handlers with specific HTTP method and URL. So, it gives you an ability to make `GET 200 /feeds` and `GET 404 /feeds`. It also support URL parameter like this `/feeds/:feedId/like`.

Now update the test like this...

```jsx
// Import all dependencies... 
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];

const url = (path) => `https://api.domain.com/feeds/${path}`;

const defaultHandlers = [
  rest.get(url(path), (req, res, ctx) => {
    return res(ctx.json(MOCK_FEEDS));
  }),
  rest.get(url(path), () => {
    return res(
      ctx.status(404),
      ctx.json(MOCK_FEEDS_NOT_FOUND),
    );
  })
];

const server = setupServer(...defaultHandlers);

describe('Feed Feature Test', () => {
  beforeAll(() => {
    server.listen();
  });

  afterEach(() => {
    server.resetHandlers();
  });

  afterAll(() => {
    server.close();
  });

  it('should shows empty state UI when feeds not found', () => {
    // Your testing goes here... 
  });

  it('should shows users feed', () => {
    // Your testing goes here... 
  });

  it('should able to like the feed', () => {
    // Adds the "POST /feeds/:feedId/like" request handler as a part of this test.
    server.use(
      rest.post(url('feeds/:feedId/like'), (req, rest, ctx) => {
        return res(
          ctx.json({ liked: true }),
        );
      }),
    );
    // Your testing goes here...
  })
});
```

Boom! your tests are passes. Not only the tests are passes it also provide better DX because you can easily create powerful URL routing.

You can also split the handlers into another file to prevent writing the handlers repeatedly. Making it reusable across all tests.

Not only provide better DX, MSW also offers you HTTP Cookie and other HTTP features. Awesome!

## Conclusion

Mocking (and testing) HTTP request is one of challenging part, but MSW comes with powerful feature, cross-platform and use simple approach to tackle this problem.

MSW provide native-like approach by intercepting the network and returns the handlers we made instead of the real one. Prevent you to pollute the `window.fetch` by mocking it and potentially caused bugs.

{% embed https://github.com/mswjs/msw %}

<a href="https://www.buymeacoffee.com/thexdev"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a macbook&emoji=💻&slug=thexdev&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff"></a>