DEV Community

Cover image for Test React component that fetching data
M. Akbar Nugroho
M. Akbar Nugroho

Posted on

Test React component that fetching data

TL;DR

We are going to use Mock Service Worker 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 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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

// 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... 
  });
});
Enter fullscreen mode Exit fullscreen mode

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, you need to refactor the test.

// 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... 
  });
});
Enter fullscreen mode Exit fullscreen mode

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

// 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...
  })
});
Enter fullscreen mode Exit fullscreen mode

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://id.pinterest.com/pin/784259722590566564/

The Solution

Luckly, there is a tool called Mock Service Worker. 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.

npm i -D msw
Enter fullscreen mode Exit fullscreen mode

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...

// 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...
  })
});
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo mswjs / msw

Seamless REST/GraphQL API mocking library for browser and Node.js.


Mock Service Worker logo

Mock Service Worker

Mock Service Worker (MSW) is a seamless REST/GraphQL API mocking library for browser and Node.js.

Package version Downloads per month Discord server


Features

  • Seamless. A dedicated layer of requests interception at your disposal. Keep your application's code and tests unaware of whether something is mocked or not.
  • Deviation-free. Request the same production resources and test the actual behavior of your app. Augment an existing API, or design it as you go when there is none.
  • Familiar & Powerful. Use Express-like routing syntax to capture requests. Use parameters, wildcards, and regular expressions to match requests, and respond with necessary status codes, headers, cookies, delays, or completely custom resolvers.

"I found MSW and was thrilled that not only could I still see the mocked responses in my DevTools, but that the mocks didn't have to be written in a Service Worker and could instead live alongside the rest of my app.

Top comments (2)

Collapse
 
kettanaito profile image
Artem Zakharchenko

Thanks for writing this piece, Akbar!

Collapse
 
thexdev profile image
M. Akbar Nugroho

You're welcome, Artem :)