Develop and test React apps with React Query, MSW and React Testing Library

denniskortsch

Dennis Kortsch

Posted on May 1, 2021

Develop and test React apps with React Query, MSW and React Testing Library

In this article we will develop a basic CRUD React app without having an API in place. Instead we will make use of Mock Service Worker to intercept & mock our fetch calls. React Query will be used as a data fetching library and we will follow a test-first approach using React Testing Library.

React-Query: For data fetching.
MSW: To intercept & mock our API calls.
React Testing Library: Write our tests.


Let's imagine a scenario where you have the specifications and requirements for your UI already but the API your app is supposed to interact with is not ready yet. Only the contract itself is already defined.

The API is roughly defined as:

GET /users, returns all users 
GET /users/:id returns a user by id
POST /users, creates a new user
PUT /users/:id, updates an existing user by id
DELETE /users/:id, deletes an existing user by primary key.
Enter fullscreen mode Exit fullscreen mode

So it is a basic Create Read Update Delete feature set.

Hence our app will have the following features:

  • list users with user name
  • show a specific user details
  • update a specific user
  • create a new user
  • delete user

Design TRIGGER Warning: For the sake of simplicity we will not care about Design or UX in this guide. We will focus solely on raw feature demonstration. So be warned, this will look like ๐Ÿ’ฉ!

The Setup

Start with creating a create-react-app:

npx create-react-app react-tdd
Enter fullscreen mode Exit fullscreen mode

And install our extra dependencies:

yarn add react-query

yarn add -D msw @mswjs/data

Clean up and React Query

Let's get at least the basic app foundation going before writing our first tests. First let's rip out everything we don't need from src/App.js, add a QueryClientProvider from react-query and a placeholder Users component.

import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Users />
    </QueryClientProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Users.js

export function Users() {
  return <div>Users</div>;
}
Enter fullscreen mode Exit fullscreen mode

Get Mock Service Worker up and running

Because we are not developing against an API and we also don't want to mock our fetch calls nor react-query itself we use msw to intercept fetch calls and return mock data. To set up msw we first need to run its initial setup script which will create the service worker script for us.

npx msw init public/ --save

Next we create 3 new files:

src/mocks/db.js.

import { factory, primaryKey } from '@mswjs/data';

export const mockUsers = [
  {
    id: '1',
    name: 'Alice',
    email: 'alice@aol.com',
  },
  {
    id: '2',
    name: 'Bob',
    email: 'bob@aol.com',
  },
  {
    id: '3',
    name: 'Dennis',
    email: 'dennis@aol.com',
  },
];

// Create a "db" with an user model and some defaults
export const db = factory({
  user: {
    id: primaryKey(),
    name: () => 'Firstname',
    email: () => 'email@email.com',
  },
});

// create 3 users
mockUsers.forEach((user) => db.user.create(user));

Enter fullscreen mode Exit fullscreen mode

Here we created some fake/mock data and then made use of MSW's data library to create an in-memory database. This will allow us to read & change data while developing/testing our app, almost as if we were interacting with a real DB.

src/mocks/server.js

import { setupServer } from 'msw/node';
import { db } from './db';

// for node/test environments
export const server = setupServer(...db.user.toHandlers('rest', 'http://localhost:8000/api/'));

Enter fullscreen mode Exit fullscreen mode

src/mocks/browser.js

import { setupWorker } from 'msw';
import { db } from './db';

// for browser environments
export const worker = setupWorker(...db.user.toHandlers('rest', 'http://localhost:8000/api/'));

Enter fullscreen mode Exit fullscreen mode

Then we also create 2 request handlers that will intercept any call to the specified URL. A worker for browser environments which can be used in Browser tests (e.g. Cypress) or during development in general. And one server for node environments which will be used in our Testing Library tests.

We also make use of the toHandlers() utility which takes a DB model, User in this case, and creates all the handlers for the usual CRUD operations automagically. This does exactly match our real API's specifications. What a lucky coincidence!

With that in place we can connect it to our app & test runner.

For tests we can use src/setupTests.js:

import '@testing-library/jest-dom';
import { server } from './mocks/server.js';

// Establish API mocking before all tests.
beforeAll(() => server.listen());
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished.
afterAll(() => server.close());
Enter fullscreen mode Exit fullscreen mode

For our browser environments we call worker.start as soon as possible in src/App.js:

import { QueryClient, QueryClientProvider } from 'react-query';
import { Users } from './Users';


+ if (process.env.NODE_ENV === 'development') {
+  const { worker } = require('./mocks/browser');
+  worker.start();
+ }

const queryClient = new QueryClient();
Enter fullscreen mode Exit fullscreen mode

Now any matching call http://localhost:8000/api/*, our imaginary API, will be intercepted and mock data will be returned - in tests AND in the real app if we would start the development server with yarn start.

First test

We have set up the base of our app and configured MSW. This would be a good time to start and actually develop our UI. For that we will write a test first. It will fail (๐Ÿ”ด) at first and we will implement the actual code to make it pass (๐ŸŸข) afterwards. That will be the flow we will use to implement all the following features as well.

From now on we can leave yarn test and yarn start running in parallel to watch our tests and develop our app in the browser.

Let's assume our users list will have a loading state while loading users.

Users.test.js

import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Users } from './Users';

describe('Users', () => {
  test('renders loading', async () => {
    const queryClient = new QueryClient();
    render(
      <QueryClientProvider client={queryClient}>
        <Users />
      </QueryClientProvider>
    );
    await waitFor(() => {
      expect(screen.getByText('Loading Users...')).toBeInTheDocument();
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Our test fails (๐Ÿ”ด) with Unable to find an element with the text: Loading Users.... as expected. Now we try to get it to pass.

In src/Users.js we make use of useQuery and a fetch helper function getUsers to call our users API endpoint at /api/users. Eventually we handle the isLoading state.

import { useQuery } from 'react-query';

async function getUsers() {
  try {
    const data = await fetch(`http://localhost:8000/api/users`);
    if (!data.ok) {
      throw new Error(data.status);
    }
    const json = await data.json();
    return json;
  } catch (error) {
    console.log(error);
  }
}

export function Users() {
  const { isLoading } = useQuery('users', getUsers);

  if (isLoading) {
    return <div>Loading Users...</div>;
  }
  return <div>Users</div>;
}
Enter fullscreen mode Exit fullscreen mode

Our tests should pass now (๐ŸŸข).

Screenshot 2021-05-01 at 13.40.44

Next feature is actually showing the list of users. Again, we write our test first.

In Users.test.js we expect the names of all our mock users to be displayed.

import { mockUsers } from './mocks/db';

...


test('lists users', async () => {
    const queryClient = new QueryClient();
    render(
      <QueryClientProvider client={queryClient}>
        <Users />
      </QueryClientProvider>
    );

    await waitFor(() => {
      mockUsers.forEach((mockUser) => {
        expect(screen.getByText(mockUser.name, { exact: false })).toBeInTheDocument();
      });
    });
 });
Enter fullscreen mode Exit fullscreen mode

It fails (๐Ÿ”ด) and we implement the correct code to make it pass.

export function Users() {
  const { isLoading, data: users } = useQuery('users', getUsers);

  if (isLoading) {
    return <div>Loading Users...</div>;
  }

  return (
    <>
      <div>Users</div>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            <div>Name: {user.name}</div>
          </li>
        ))}
      </ul>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tests pass (๐ŸŸข) and we can go on implement the next feature.

Our app should have the functionality for creating users as well. You know the drill: failing test first!

Users.test.js

test('create new user', async () => {
    const queryClient = new QueryClient();
    render(
      <QueryClientProvider client={queryClient}>
        <Users />
      </QueryClientProvider>
    );

    const createButton = await screen.findByText('Create new User');

    fireEvent.click(createButton);

    const newUserInList = await screen.findByText('Name: John');
    expect(newUserInList).toBeInTheDocument();
  });
Enter fullscreen mode Exit fullscreen mode

And the matching implementation. We create a new component CreateUser.

import { useMutation, useQueryClient } from 'react-query';

async function createUser(newUser) {
  try {
    const data = await fetch(`http://localhost:8000/api/users`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(newUser),
    });
    if (!data.ok) {
      throw new Error(data.status);
    }
    const json = await data.json();
    return json;
  } catch (error) {
    console.log(error);
  }
}

export function CreateUser() {
  const queryClient = useQueryClient();
  const createUserMutation = useMutation((newUser) => createUser(newUser), {
    onSuccess: () => {
      queryClient.invalidateQueries('users');
    },
  });

  return (
    <button
      onClick={() =>
        createUserMutation.mutate({
          id: '4',
          name: 'John',
          email: 'john@aol.com',
        })
      }
    >
      Create new User
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

We use React-Query's useMutation and a helper function createUser to make a POST call to our API. onSuccess we invalidate our users data to trigger a refetch. For simplicity we hard code the new users info.

Our test passes (๐ŸŸข).

At this point I think it is clear how a possible workflow could look like and what the possibilities & advantages of having a mocked interactive API are. Our UI is ready to be connected to a real API once it is implemented.

I won't go through testing all the other features here but instead link to a repository with the completed code in place.

Or maybe you want to take it as a challenge and complete the rest of the tests yourself? Here are some ideas that should probably be implemented next:

  • We are still missing "Show a user's detailed info", "Updating a user" and "Deleting a user"
  • What about Error handling and states?
  • Another thing that already stands out is that there could be a lot of repetition with the fetch helper functions. Maybe refactor and find a better abstraction for it?

Repository: : https://github.com/DennisKo/react-msw-demo

I am open for questions and improvements! Contact me here or on Twitter:

๐Ÿฆ https://twitter.com/DennisKortsch

๐Ÿ’– ๐Ÿ’ช ๐Ÿ™… ๐Ÿšฉ
denniskortsch
Dennis Kortsch

Posted on May 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ยฉ TheLazy.dev

About