Frontend Integration Test with Graphql Mocking
Tags: frontend, testing, playwright, graphql
Published: 2023-04-18 09:04:53

> Last Updated: 2023/05/15

## Why Frontend Integration Test ?

In accordance with the [Frontend Testing Philosophy](https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications) outlined by Kent C. Dodds, the most valuable type of test for frontend developers to write is an integration test. This type of test strikes a good balance between effort and code quality confidence.

So, what should we test in frontend integration tests to achieve the best confidence and minimize effort in maintaining and coding these tests?

In my opinion, UI operations and API calls are the most important aspects, as they are fully controlled by the frontend application. Anything that occurs after the API call requires backend dependencies, and we have no responsibility to ensure that the database update logic or data synchronization works correctly. Therefore, testing any UI updates based on data fetching is not worth the effort. Additionally, we need to implement some part of the backend logic to return the newest data we just sent, which is not worth the effort either.

## Playwright as a solution

As the philosophy presented, we need to make our test and environment as close to the real production environment as possible. Instead of using an interceptor like mock-axios, which would be injected into the frontend code, we can choose a testing framework like [Playwright](https://playwright.dev/) that provides API mocking on the browser side. This means that passing test cases ensure that our web app will work perfectly in the production environment, assuming that the backend service is functioning properly.

Please refer to the official document to set up Playwright in your current project.

Once you have set up the basic testing environment, you can write a test case that triggers an API request with Playwright's [API mocking method](https://playwright.dev/docs/mock) (keep in mind that it mocks the API at the browser layer, so you don't need to modify your frontend codebase).

Here's an example:

```ts
import { test, expect } from '@playwright/test';

test('has title', async ({ page }) => {
  await page.route('https://backend.api', async (route) => {
    const json = {
      message: { msg: "It's a test message!" },
    };
    await route.fulfill({ json });
  });

  await page.goto('https://myApp.frontend/');

  await expect(page).toHaveTitle(/MyApp/);
});
```

However, writing API mocking for every API in each test can be tedious. Instead of using the beforeEach hook, you can use [Fixtures](https://playwright.dev/docs/test-fixtures) in Playwright to create a global mocking layer for your default API, making code maintenance easier.

Here's an example:

```ts
// helper.ts
import { test as baseTest } from '@playwright/test';

export const test = baseTest.extend({
  model: async ({ page }, use) => {
    await page.route('https://backend.api', async (route) => {
      const json = {
        message: { msg: "It's a test message!" },
      };
      await route.fulfill({ json });
    });
  },
});

//example.test.ts
import { test } from './helper';

test('should add an item', async ({ model }) => {
  // ...
});

test('should remove an item', async ({ model }) => {
  // ...
});

// All tests will have the mock API effect!
```

## Make Playwright works with Graphql

GraphQL is built on top of the HTTP POST request, so we only need to parse the request body to extract information about the request. To simplify this process, we can use a popular mock helper library -  [MSW](https://mswjs.io/docs/api/graphql/query) as a GraphQL parser (which we'll explore in more detail in the next post).

```ts
// handleRoute.ts
import type { Route } from '@playwright/test';
import type { RequestHandler } from 'msw';
import { Headers } from 'headers-polyfill';
import { handleRequest, MockedRequest, type MockedResponse } from 'msw';
import EventEmitter from 'events';

const emitter = new EventEmitter() as Parameters<typeof handleRequest>[3];

export const handleRoute = async (route: Route, handlers: RequestHandler) => {
  try {
    const request = route.request();

    let requestHeaders = {};
    try {
      requestHeaders = await request.allHeaders();
    } catch {}

    const headers = new Headers(requestHeaders);
    const method = request.method();
    const url = new URL(request.url());
    const postData = request.postData();

    const mockedRequest = new MockedRequest(url, {
      method,
      headers,
      body: postData ? Buffer.from(postData) : undefined,
    });

    const handleMockResponse = async ({
      status,
      headers,
      body,
      delay,
    }: MockedResponse) => {
      if (delay) {
        await new Promise((resolve) => {
          setTimeout(resolve, delay);
        });
      }

      return route.fulfill({
        status,
        body: body ?? undefined,
        contentType: headers.get('content-type') ?? undefined,
        headers: headers.all(),
      });
    };

    await handleRequest(
      mockedRequest,
      [],
      {
        onUnhandledRequest: () => {
          console.error('no match handler');
        },
      },
      emitter,
      {
        resolutionContext: {
          /**
           * @note Resolve relative request handler URLs against
           * the server's origin (no relative URLs in Node.js).
           */
          baseUrl: url.origin,
        },
        onMockedResponse: handleMockResponse,
      },
    );
  } catch (err) {
    console.error(err);
  }
};
```

```ts
// helper.ts

import { test as baseTest, expect as baseExpect } from '@playwright/test';
import type { RequestHandler } from 'msw';
import { handleRoute } from './handleRoute';

type fixtures = {
  model: { addHandler: (handlers: RequestHandler) => void };
};

export const test = baseTest.extend<fixtures>({
  model: [
    async ({ page }, use) => {
      // if you want to have some default hadlers, you can put them into this array
      const handlers: RequestHandler[] = [];

      await page.route('**/graphql', (route) => handleRoute(route, handlers));

      use({
        addHandler: (handler: RequestHandler) => handlers.unshift(handler),
      });
    },
    { auto: true },
  ],
});
```

With above configuration we finally can add handler directly in our test!

```ts
  //example.test.ts
  import { test } from './helper';
  import { graphql } from 'msw';

  test('example test', async ({ page, model }) => {
    model.addHandler(
      graphql.query('GetAllUsers', (req, res, ctx) => {
        return res(
          ctx.data({
            users: [
              {
                firstName: 'John',
                lastName: 'Maverick',
              },
              {
                firstName: 'Cathaline',
                lastName: 'McCoy',
              },
            ],
          }),
        );
      }),
    );

    await page.goto('http://frontend,app/users');
  });
```

## The Frustration of writing Mocking Data

Writing mock data for integration tests can be frustrating, especially if your service keeps evolving, and each field of the API could change. We need a better way to make our lives easier.

Only when writing or maintaining tests doesn't consume too much time, can we push our test coverage to the next level. Fortunately, we have many tools available in the community for automatically generating RESTful APIs and GraphQL mock data with pre-defined schemas. We can rely on these tools to keep the implementation between the frontend and backend in sync.



## Graphql Codegen for generating Mock Data

When your project starts to adopt GraphQL, you'll find many codegen tools that can help you improve your code quality. Today, we'll use a mature codegen tool called [GraphQL Code Generator](https://the-guild.dev/graphql/codegen).

> Please follow the official document to set up the environment.
> 
We will depend on two useful plugins: [TypeScript Mock Data](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-mock-data) and [TypeScript MSW](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-msw).

### TypeScript Mock Data
With this plugin, you can generate mock data statically or dynamically based on your GraphQL schema using well-known mock data libraries such as Casual or Faker. It also makes writing mock data with default values easier. You can specify only the crucial attribute related to the test. For example:

```graphql
scalar AWSTimestamp

type User {
  id: ID!
  name: String!
  avatar: Avatar
  status: Status!
  updatedAt: AWSTimestamp
}
```

```ts
// generated mock data configuration with Casual static data
export const aUser = (overrides?: Partial<User>): User => {
  return {
    id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : 'a5756f00-41a6-422a-8a7d-d13ee6a63750',
    name: overrides && overrides.hasOwnProperty('name') ? overrides.name! : 'libero',
    avatar: overrides && overrides.hasOwnProperty('avatar') ? overrides.avatar! : anAvatar(),
    status: overrides && overrides.hasOwnProperty('status') ? overrides.status! : Status.Online,
    updatedAt: overrides && overrides.hasOwnProperty('updatedAt') ? overrides.updatedAt! : 1458071232,
  };
};

// In test case
import * as mockData from './generated'
const user = mockData.aUser({ name: 'johndoe' });
```

### TypeScript Msw
This plugin integrates Graphql Codegen and [MSW](https://mswjs.io/), allowing for the generation of API mocking helpers with strong types. This provides maximum benefits for writing API mocking. For example:

```graphql
query GetUser($id: ID!) {
  getUser(id: $id) {
    name
    id
  }
}
```

```ts
mockGetUserQuery((req, res, ctx) => {
  const { id } = req.variables
 
  return res(
    ctx.data({
      getUser: { name: 'John Doe', id }
    })
  )
})
```

With these two brilliant plugins, we can write our mock APIs smoothly without losing any of the side properties that the API should respond with. For example:

```ts
 mockGetUserQuery((req, res, ctx) => {
    const { id } = req.variables
 
    return res(
      ctx.data({
        getUser: mockData.aUser({ login: 'johndoe' });
      })
    )
  })
```

## Combine with Playwright Graphql Mocking

Combine the example in the previous part, we can write our code quite smoothly like follow:

```ts
//example.test.ts
import { test } from './helper';
import * as mock from './generated'

test('example test', async ({ page, model }) => {
  model.addHandler(
    mock.mockGetUserQuery((req, res, ctx) => {
      const { id } = req.variables
     
      return res(
        ctx.data({
          getUsers: [
            mockData.aUser({ name: 'johndoe' }),
            mockData.aUser({ name: 'charle' })
          ]
        })
      )
    })
  );

  await page.goto('http://frontend,app/users');
 });
```

### Testing Mutation
So far, we have only demonstrated the use of mock data with queries. However, in most cases, we also need to test mutations, such as form submissions or order placements.

Since we can dynamically control API handlers, we can inject mutations into our handlers and test if the variables being passed to the backend are correct.

```graphql
mutation UpdateUser($id: ID!, $name: String) {
  updateUser(input: { id: $id, name: $name }) {
    id
  }
}
```

```ts
//example.test.ts
import { test } from './helper';
import * as mock from './generated'

test('update user', async ({ page, model }) => {
  const targetUser = mock.aUser();
  const editedUserName = 'test_user_name';

  let isVarsRight = false;

  model.addHandler(
    mock.mockUpdateUser((req, res, ctx) => {
      const { id, name } = req.variables;
      expect(id).toEqual(targetUser.id);
      expect(name).toEqual(editedUserName);
      isVarsRight = true;
      return res(ctx.data({} as any));
    }),
  );

  await page.goto('http://frontend,app/user');

  await page.getByLabel('name').fill(editedUserName);
  await page.getByTestId('SaveIcon').click();

  expect(isVarsRight).toBeTruthy();
});
```

You may find it odd that we need a flag called `isVarsRight` and `expect` it at the end of the test. This is because the expect function in the handler's callback runs outside of the test process, and any errors it produces will not be captured by Playwright.

## Conclusion

This article only showcases some of the potential of using Graphql Codegen with Playwright to significantly improve the developer experience when writing frontend integration tests. As Graphql becomes more and more popular, I'm excited to see what other solutions the community will come up with. However, our team is quite satisfied with our current workflow. I hope that these articles can serve as a starting point or source of inspiration for you to build a better frontend test infrastructure!