Implementing and Unit testing Graphql in React using msw
Tags: react, graphql, jest, msw
Published: 2023-05-23 04:44:55

There are pretty good articles on using **msw** for graphql / rest unit testing for react out there. And this is my implementation with a protip at the end :wink: so don't miss a detail :blush:

**TL'DR** If you want to skip this biggg reading, here is the [source code](https://github.com/KiranMantha/react-graphql-unittesting)

This is a no-explanation article. simply copy paste the code and it will work.

This implementation has 3 stages.

Stage 1 => Implementing graphql
Stage 2 => Writing unit tests with msw
Stage 3 => Using msw as development server

As ususal let's start with Stage 1.

## Stage 1: Implementing graphql

<u>Step - 1: App scaffolding </u>
Create a react app using CRA and typescript, assuming you installed cra globally:

run `yarn create react-app my-app --template typescript`

<u>Step - 2: Folder structure</u>
This is my folder structure
```
.
├── src/
│   ├── api/
│   │   ├── gql/
│   │   │   ├── interceptors/
│   │   │   │   ├── interceptor.ts
│   │   │   │   ├── utils.ts
│   │   │   │   └── index.ts
│   │   │   ├── models/
│   │   │   │   ├── users.model.ts
│   │   │   │   └── index.ts
│   │   │   ├── queries/
│   │   │   │   ├── users.ts
│   │   │   │   └── index.ts
│   │   │   └── requests/
│   │   │       ├── users.ts
│   │   │       └── index.ts
│   │   ├── mockhandler/
│   │   │   ├── queries
│   │   │   └── index.ts
│   │   └── testUtils/
│   │       └── index.tsx
│   ├── Users.tsx
│   ├── Users.spec.tsx
│   ├── App.tsx
│   ├── App.spec.tsx
│   ├── index.tsx
│   └── jest.setup.ts
├── jest.config.ts
├── setupApiTests.ts
├── global.d.ts
└── package.json
```

<u>Step - 3: Packages</u>
let's add following packages
`yarn add @tanstack/react-query graphql-request @testing-library/jest-dom @testing-library/react @testing-library/user-event @testing-library/jest-dom @types/jest ts-jest ts-node msw identity-obj-proxy`

<u>Step - 4: Setting up Jest</u>
Add the following to 

- **_jest.config.ts_**
```javascript
//jest.config.ts

export default {
  displayName: "react-graphql-unittesting",
  testEnvironment: "jsdom",
  transform: {
    "^.+\\.tsx?$": "ts-jest",
  },
  setupFilesAfterEnv: [
    "<rootDir>/src/jest.setup.ts",
    "<rootDir>/setupApiTests.ts",
  ],
  moduleNameMapper: {
    '^.+\\.(css|less)$': 'identity-obj-proxy'
  },
  collectCoverageFrom: [
    'src/**/*{js,jsx,ts,tsx}',
    '!src/**/index.ts',
    '!<rootDir>/node_modules/',
  ],
  coverageReporters: ["clover", "json", "lcov", "text", "html"],
};
```
- **_setupApiTests.ts_**
```javascript
//setupApiTests.ts

const { setupServer } = require('msw/node');
const { handler } = require('./src/api/mockHandler');
const server = setupServer(...handler);
// 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());

export {};

```
- **_jest.setup.ts_**
```javascript
//jest.setup.ts

import '@testing-library/jest-dom';
jest.setTimeout(30000);
```

<u>Step - 4: Implementing sample graphql api</u>

Let's use [graphqlZero](https://graphqlzero.almansi.me/) for sample graphql api.

a. lets add interceptor
```typescript
// interceptor/utils.ts

enum QueryType {
  QUERY = 'query',
  MUTATION = 'mutation'
}

const extractNameFromQuery = (query: string) => {
  const keywordIndex =
    query.indexOf(QueryType.QUERY) !== -1
      ? query.indexOf(QueryType.QUERY) + QueryType.QUERY.length
      : query.indexOf(QueryType.MUTATION) + QueryType.MUTATION.length;
  return query.substring(keywordIndex, query.indexOf('(')).replace(/ /g, '');
};

export { extractNameFromQuery };

// interceptor/interceptor.ts

import { GraphQLClient } from 'graphql-request';
import { extractNameFromQuery } from './utils';

const getGraphQLClient = (
  _queryName: string,
  optionalHeader?: Record<string, string | number | boolean>
): GraphQLClient => {
  let globalHeaders = {};

  if (optionalHeader) {
    globalHeaders = {
      ...optionalHeader,
      ...globalHeaders
    };
  }
  return new GraphQLClient('https://graphqlzero.almansi.me/api', {
    headers: { ...globalHeaders } as Record<string, string>
  });
};

const GQLInteraction = async <T,>(
  schema: string,
  variables?: Record<string, string[] | number | number[] | unknown> | undefined,
  optionalHeader: Record<string, string | number | boolean> = {}
): Promise<T> => {
  try {
    const queryDescription = extractNameFromQuery(schema);
    const client = getGraphQLClient(queryDescription, { ...optionalHeader });
    return await client.request(schema, variables);
  } catch (err) {
    console.log('error', err);
    throw err;
  }
};

export { GQLInteraction };

// interceptor/index.ts

export * from './interceptor';

```

b. lets add the query
```typescript
// queries/users.ts

export const getUser = `
query user($id: ID!){
  user(id: $id) {
    id
    username
    email
    address {
      geo {
        lat
        lng
      }
    }
  }
}
`;

// queries/index.ts

export * from './users';
```
c. lets add the response type
```typescript
// models/users.model.ts

export interface User {
  id: string;
  username: string;
  email: string;
  address: {
    geo: {
      lat: string;
      lng: string;
    };
  };
}

export interface UserResponse {
    user: User
}

// models/index.ts

export * from "./users.model";
```

d. lets create a request to fetch the user
```typescript
// requests/users.ts

import { UseQueryResult, useQuery } from "@tanstack/react-query";
import { GQLInteraction } from "../interceptors";
import { User, UserResponse } from "../models";
import { getUser } from "../queries";

export const useGetUser = (userid: string): UseQueryResult<User, unknown> => {
  const options = {
    refetchOnWindowFocus: false,
    retry: 0,
    select: (response: UserResponse): User => {
      return response.user;
    },
  };

  return useQuery(
    ["user"],
    () => GQLInteraction<UserResponse>(getUser, { id: userid }),
    options
  );
};


// requests/index.ts

export * from './users';
```

<u>Step - 5: Consuming api in component</u>
a. consume request in Users component
```typescript
// Users.tsx

import { useGetUser } from "./api/gql/requests";

export function Users() {
  const { data, isFetching } = useGetUser("1");
  return !isFetching && data ? (
    <h1 data-testid="username">{data.username}</h1>
  ) : (
    <div data-testid="loading">loading</div>
  );
}

```

b. update App component as below
```typescript
// App.tsx

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./App.css";
import { Users } from "./Users";

function App() {
  const queryClient = new QueryClient();

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

export default App;
```
Finally we integrated graphql in our react application. 

Oops.. App.spec.tsx is failing. lets modify it as below:

```typescript
import { render, screen } from "@testing-library/react";
import App from "./App";

describe("App", () => {
  it("should renders learn react link", () => {
    render(<App />);
    const loaderElement = screen.getByTestId("loading");
    expect(loaderElement).toBeInTheDocument();
  });
});

```

Huff now looks good.

Now it's time for implementing Stage 2.

## Stage 2: Writing unit tests with msw

<u>Step - 1: implementing mock handler.</u>

In stage 1 when we run unit tests, jest didn't get terminated properly. this is because our component tries to make api call as if in real browser. But it didn't get any response hence jest didn't get terminated properly. To fix this lets implement the mock handlers that provide mock api response.

```typescript
// mockhandler/queries/users.ts

import { graphql } from "msw";

export const UsersQueries = [
  graphql.query("user", (_req, res, ctx) => {
    return res(
      ctx.data({
        user: {
          id: "1",
          username: "Antonette",
          email: "Shanna@melissa.tv",
          address: {
            geo: {
              lat: -43.9509,
              lng: -34.4618,
            },
          },
        },
      })
    );
  }),
];


// mockhandler/queries/index.ts

export * from './users';

// mockhandler/index.ts

import { UsersQueries } from "./queries";

const handler = [...UsersQueries];

export { handler };
```

That's it. now if we run tests again, the msw provides mock api response to `user` query and jest will terminate properly.

As usual there's scope for improvement.

Lets refactor this a little bit.

```typescript
// testUtils/index.tsx

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ComponentType, ReactNode } from "react";

const createTestQueryClient = () =>
  new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
    },
  });

export function createWrapper(node: ReactNode): ComponentType {
  const testQueryClient = createTestQueryClient();
  return function MockComponent() {
    return (
      <QueryClientProvider client={testQueryClient}>{node}</QueryClientProvider>
    );
  };
}

```

Lets update Users component

```typescript
// Users.tsx

import { useGetUser } from "./api/gql/requests";

export function Users() {
  const { data, isFetching } = useGetUser("1");
  return (
    <div data-testid="users-component">
      {!isFetching && data ? (
        <h1 data-testid="username">{data.username}</h1>
      ) : (
        <div data-testid="loading">loading</div>
      )}
    </div>
  );
}
```

Let's update the App.spec.ts

```typescript
// App.spec.ts

import { render, screen } from "@testing-library/react";
import App from "./App";

describe("App", () => {
  it("should renders learn react link", () => {
    render(<App />);
    const usersComponent = screen.getByTestId("users-component");
    expect(usersComponent).toBeInTheDocument();
  });
});

```

Now let's create a spec file for users component.

```typescript
// Users.spec.ts

import { render, screen, waitFor } from "@testing-library/react";
import { Users } from "./Users";
import { createWrapper } from "./api/testUtils";

describe("Users", () => {
  const Component = createWrapper(<Users />);

  it("should render users component", () => {
    render(<Component />);
    expect(screen.getByTestId("users-component")).toBeInTheDocument();
  });

  it("should display user name from api", async () => {
    render(<Component />);
    await waitFor(() => {
      expect(screen.getByText(/Antonette/i)).toBeInTheDocument();
    });
  });
});

```

The `createWrapper` function will help to wrap any individual component which consume graphql in unit tests.

Awesome. all the happy flow is done. but what about error scenerio??

Now it's time for **protip**

<u>Step - 2: Protip</u>

let's update the setupApiTests file

```typescript
// setupApiTests.ts

const { setupServer } = require("msw/node");
const { handler } = require("./src/api/mockHandler");
const server = setupServer(...handler);
global.mswServer = 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());

export {};

```

create `global.d.ts` in root and add the following:

```typescript
declare module globalThis {
  var mswServer: SetupServer;
}
```

update tsconfig include property:
```json
// tsconfig.json
{
   ...
   "include": ["src", "global.d.ts"]
}
```

finally update testutils as below:

```typescript
// testUtils/index.tsx

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { graphql } from "msw";
import { SetupServer } from "msw/node";
import { ComponentType, ReactNode } from "react";

const createTestQueryClient = () =>
  new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
    },
  });

export function createWrapper(node: ReactNode): ComponentType {
  const testQueryClient = createTestQueryClient();
  return function MockComponent() {
    return (
      <QueryClientProvider client={testQueryClient}>{node}</QueryClientProvider>
    );
  };
}

export function mockApiResponse(
  queryName: string,
  response: Record<string, unknown> | null,
  throwError = false
) {
  (global.mswServer as SetupServer).use(
    graphql.query(queryName, (_req, res, ctx) => {
      if (throwError) {
        return res(ctx.errors([]));
      }
      return res(ctx.data(response as Record<string, unknown>));
    })
  );
}

```

Now it's time to test -ve scenerio in users component:

```typescript
// Users.tsx

import { useGetUser } from "./api/gql/requests";

export function Users() {
  const { data, isFetching, error } = useGetUser("1");
  return (
    <div data-testid="users-component">
      {!isFetching && data ? (
        <h1 data-testid="username">{data.username}</h1>
      ) : (
        <div data-testid="loading">loading</div>
      )}
      {error ? (
        <div data-testid="api-error">Oops something went wrong</div>
      ) : null}
    </div>
  );
}


// Users.spec.tsx

import { render, screen, waitFor } from "@testing-library/react";
import { Users } from "./Users";
import { createWrapper, mockApiResponse } from "./api/testUtils";

describe("Users", () => {
  const Component = createWrapper(<Users />);

  it("should render users component", () => {
    render(<Component />);
    expect(screen.getByTestId("users-component")).toBeInTheDocument();
  });

  it("should display user name from api", async () => {
    render(<Component />);
    await waitFor(() => {
      expect(screen.getByText(/Antonette/i)).toBeInTheDocument();
    });
  });

  it("should display api-error placeholder when api throws error", async () => {
    mockApiResponse("user", null, true);
    render(<Component />);
    await waitFor(() => {
      expect(screen.getByTestId("api-error")).toBeInTheDocument();
    });
  });
});
```

And that's how we tested api failure case too.

That's a lot of copy paste but i promise this surely helps you alot.

Now its time for final stage.

## Stage 3: Using msw as development server

With very minimal changes we can convert the till now msw setup as development server as well.

Let's see how to do that.

In behind the screens, msw rely on serviceWorker in application public folder to serve mock api responses (check [here](https://mswjs.io/docs/cli/init) to know more about this). To setup this service worker let's run the below command (assuming you have msw installed globally. else move the below command to a script in package json and run it).

`msw init public --save`

This command essentially creates the service worker for us in cra `public` folder. 

After this lets tweak the `index.tsx` file as below

```typescript
// index.tsx
...

import { setupWorker } from 'msw';
import { handler } from './api/mockhandler';

if(process.env.NODE_ENV === 'development') {
   setupWorker(...handler).start();   
}

...
```

That's it. The mock server is up and running. To test this, turn off internet and run the app you will see the data displayed from msw :blush:

The process is same for `graphql.mutation` too.

Are you using rest instead of graphql, no worries. in mockhandler instead of using `graphql.query` use `rest` from msw. The official msw docs are good enough to understand using rest with msw.

And That's the end of this article.

Thanks,
Kiran :wave: