Using msw to test React with GraphQL effectively
Tags: graphql, react, testing
Published: 2021-09-17 19:53:52

---
title: Using msw to test React with GraphQL effectively
published: true
description: 
tags: #graphql #react #testing
---

Testing React application is now easier than before thanks to the tools like jest, testing-library, jest-dom. But it gets kinda hard when you have to deal with side effects, especially api call. In this article, I'll show you how to test React with GraphQL easily and effectively by using msw.

# Don't mock your client

When you search how to test React Component with GraphQL, you'll might see the articles or guides that shows how to mock graphql client or it's Provider.

```ts
import TestRenderer from 'react-test-renderer';
import { MockedProvider } from '@apollo/client/testing';
import { GET_DOG_QUERY, Dog } from './dog';

const mocks = [];

it('renders without error', () => {
  const component = TestRenderer.create(
    <MockedProvider mocks={mocks} addTypename={false}>
      <Dog name="Buck" />
    </MockedProvider>,
  );

  const tree = component.toJSON();
  expect(tree.children).toContain('Loading...');
});
```

This is how [apollo client instructs](https://www.apollographql.com/docs/react/development-testing/testing/).

And for urql, it also instructs the way to mock client.

```ts
import { mount } from 'enzyme';
import { Provider } from 'urql';
import { never } from 'wonka';
import { MyComponent } from './MyComponent';

it('renders', () => {
  const mockClient = {
    executeQuery: jest.fn(() => never),
    executeMutation: jest.fn(() => never),
    executeSubscription: jest.fn(() => never),
  };

  const wrapper = mount(
    <Provider value={mockClient}>
      <MyComponent />
    </Provider>
  );
});
```

Well, what's wrong with mocking?

1. It's tied to particular GraphQL Client. Tests will be broken if you change the client library one to another.
2. Mocked Provider possibly works different from real Provider running on production. What if your Provider includes complex logic that would affect your app's behavior?

# MSW

https://mswjs.io/

MSW solves those problems. MSW (Mock Service Worker) is a REST/GraphQL API mocking library for browser and Node.js, that intercepts requests and act as a real server. 

MSW intercepts requests on the network level, so by using msw in your test, you don't need to mock GraphQL Client, Provider anymore!

Then let's see how to write React component tests with msw.

# Setup msw for testing

## Example App

Before dive into msw, let's see how example app looks like.

Imagine we have a scheme like

```graphql
  type Query {
    todos: [Todo!]!
  }

  type Mutation {
    saveTodo(todo: TodoInput!): Todo
  }

  type Todo {
    id: ID!
    title: String!
  }

  input TodoInput {
    title: String!
  }
```

And your app fetches todos

```
import { useQuery } from 'urql';

const TodosQuery = `
  query {
    todos {
      id
      title
    }
  }
`;

const Todos = () => {
  const [result] = useQuery({
    query: TodosQuery,
  });

  const { data, fetching, error } = result;

  if (fetching) return <p>Loading...</p>;
  if (error) return <p>Oh no... {error.message}</p>;

  return (
    <ul>
      {data.todos.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
};
```

## msw setup

Following their docs, we should specify 3 files at first. Thanks to msw, you can define mock data fully type safely.

**mocks/handlers.ts**

```ts
import { graphql } from 'msw'
import { GetTodosDocument } from 'src/generated/graphql.ts/graphql'

export const handlers = [
  graphql.query(GetTodosDocument, (req, res, ctx) =>
    res(
      ctx.data({
        todos: [todoFactory(), todoFactory()], // fully typed
      })
    )
  ),
]
```

In this file, define your default handlers, which is supposed to be used in your tests widely. Each handlers can be overwritten in each test case.

`todoFactory()` is the mock factory function. I'll explain it later but it's just a function that returns mock data of todo.

**mocks/server.ts**

```ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)
```

**jest.setup.ts**

```ts
import { server } from './mocks/server'

// 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())
```

Last two files are just template files.

## Custom Render setup

As testing-library encourages, it's useful to define custom render. You can use your Graphql Client Provider that is used in production.

```ts
import { render } from '@testing-library/react'
import { GraphQLHandler, GraphQLRequest } from 'msw'

import { UrqlClientProvider } from './components/util/UrqlClientProvider'
import { server } from './mocks/server'

export const testRenderer =
  (children: React.ReactNode) =>
  (responseOverride?: GraphQLHandler<GraphQLRequest<never>>) => {
    if (responseOverride) {
      server.use(responseOverride)
    }
    render(<UrqlClientProvider>{children}</UrqlClientProvider>)
  }
```

Here `testRenderer` can accept responseOverride, which is aimed at overriding existing handler we defined earlier in `mock/handlers.ts`.

## Write tests!

### Basic

Now it's time to write actual tests! So for the Happy Path, we don't need to override default handlers, so just call `renderPage` function without parameters.

```ts
describe('Todos Page', () => {
  const renderPage = testRenderer(<Todos />)

  it('displays fetched todo list', async () => {
    renderPage()
    const target = await screen.findAllByTestId('todo')
    expect(target.length).toBe(2)
  })
})
```

### Override handlers for edge case tests

And if you want to test edge case or when the test depends on particular mock response pattern, call `renderPage` with the handlers you want to override:

```ts
describe('Todos Page', () => {
  const renderPage = testRenderer(<Todos />)

  it('displays "No Items" when there is no todo', async () => {
    renderPage(
      // overrides existing GetTodosDocument query.
      graphql.query(GetTodosDocument, (req, res, ctx) =>
        res.once(
          ctx.data({
            todosByCurrentUser: [],
          })
        )
      )
    )
    const target = await screen.findByText('No Items')
    expect(target).toBeInTheDocument()
  })

  it('displays "completed" on the todo when fetched todo is completed', async () => {
    renderPage(
      // overrides existing GetTodosDocument query.
      graphql.query(GetTodosDocument, (req, res, ctx) =>
        res.once(
          ctx.data({
            todosByCurrentUser: [todoFactory({completed: true})],
          })
        )
      )
    )
    const todo = await screen.findByTestId('todo')
    expect(within(todo).getByText('completed')).toBeInTheDocument()
  })
})
```

### mutation test

You can test mutation call by define interceptor mock function and pass variables in your msw handler:

```ts
describe('Todos Page', () => {
  const renderPage = testRenderer(<Todos />)

  it('should create new Todo', async () => {
    const mutationInterceptor = jest.fn()
    renderPage(
      graphql.mutation(SaveTodoDocument, (req, res, ctx) => {
        mutationInterceptor(req.variables) // pass the variables here
        return res.once(
          ctx.data({
            saveTodo: {
              __typename: 'Todo',
              id: '1',
            },
          })
        )
      })
    )

    const input = screen.getByLabelText('title')
    fireEvent.change(input, { target: { value: 'test' } })
    const submitButton = screen.getByText('Submit')
    fireEvent.click(submitButton)

    await waitFor(() =>
      expect(mutationInterceptor).toHaveBeenCalledWith({
        todo: {
          title: 'test',
        },
      } as SaveTodoMutationVariables)
    )
  })
})
```

## mock factory pattern

In the example code above, I used `todoFactory()` function. Explained well in [this post](https://dev.to/davelosert/mock-factory-pattern-in-typescript-44l9), but in a nutshell, it is a helper function that produces mock data easily and flexibly.

```ts
let nextFactoryIds: Record<string, number> = {}

export function resetFactoryIds() {
  nextFactoryIds = {}
}

export function nextFactoryId(objectName: string): string {
  const nextId = nextFactoryIds[objectName] || 1
  nextFactoryIds[objectName] = nextId + 1
  return String(nextId)
}

function todoFactory(options?: Partial<Todo>): Todo {
  return {
    __typename: 'Todo',
    id: nextFactoryId('Todo'),
    title: 'test todo',
    completed: false,
    ...options,
  }
}

// usage
todoFactory()
todoFactory({completed: true})
```

I'm implementing auto incremented id here but it's optional. If you want, don't forget to reset incremented ids in afterEach.

## Summary

* Avoid mock your Graphql Client or Provider.
* MSW is a good fit for mocking graphql response.
* Mock factory pattern might help you define mock data.

You can find entire code example in my boilerplate repo:
https://github.com/taneba/fullstack-graphql-app/blob/main/frontend/src/pages/todos/index.test.tsx

I hope you enjoyed and this article helps you in some way. Happy coding!