Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Created June 22, 2021 11:32
Show Gist options
  • Save OliverJAsh/2f767b00cce6a227a196298ee42d1669 to your computer and use it in GitHub Desktop.
Save OliverJAsh/2f767b00cce6a227a196298ee42d1669 to your computer and use it in GitHub Desktop.
Unsplash: Architectural Decision Record (ADR): Mocking API requests in E2E (Cypress) tests

Mocking API requests in E2E (Cypress) tests

Problem

Testing at the network level

In our E2E tests, when the test runner navigates through the site, we don't want to make real API requests but rather we want to mock these requests so that we can easily test many different scenarios, and so that our tests are more resilient and less likely to flake if API is suffering any downtime.

Up until now, this has been achieved by "mocking the fetch function". For example:

const getSearchResults = (query) => (TEST ? Promise.resolve(mockResponse) : fetch('…'));

However, with this approach, we're not testing the network which means we are unable to catch some bugs (e.g. badly formatted request URL/headers or an issue inside our API proxy) and we are unable to test some scenarios such as network errors.

Ideally we would still make a network request but just mock the response at the network level. This would help us to increase our test coverage and debug issues in the browser's dev tools network panel.

Co-locating mock responses

If we want to write a test for an edge case, we have to extend the logic in our source code.

const getSearchResults = (query) =>
  TEST
    ? Promise.resolve(
        (() => {
          switch (query) {
            case 'empty':
              return { photos: [], collections: [] };
            case 'some-photos-but-no-collections':
              return { photos: [a, b, c, d, e, f], collections: [] };
            default:
              return mockResponse;
          }
        })(),
      )
    : fetch('…');

The definition of this mock response is located far away from the test definition. As we want to test more edge cases, these switch statements quickly build up. For this reason it would be better if we could co-locate our mock responses with the tests that use them, e.g.

it('shows the empty state when we have no results', () => {
  ApiMock.getSearchResults({ photos: [], collections: [] });

  cy.findByText('No photos');
});

Possible solutions

There are many tools which claim to help solve these problems but they are all very different and each one has its own trade-offs to consider.

The most popular solutions (nock, MSW) only work when you want to mock the response for a request which happens inside the same process, i.e. your server process must be running in the same process as your tests. They do not allow you to mock responses for requests which happen in a separate process, as is the case for our E2E tests: we want to define mock responses from our Cypress tests (which run in the browser) for requests which happen in the application server's Node process:

Cypress offers its own solution to this problem in the form of cy.intercept. Whilst this works really well for requests which happen inside of the browser, it has the same problem as tools like nock—it does not let you mock responses for requests which happen on the server side (feature request to fix this inside of Cypress). Despite the issue with cy.intercept it is still a compelling solution because it integrates nicely with Cypress (the UI shows which requests have been mocked and how many times they have been matched), and we wouldn't need to bring in any extra tools.

It might be possible to use these tools for some of our tests but we would need to significantly adapt them so that they only test client-side navigations rather than SSR.

This would add a considerable amount of boilerplate to our Cypress tests, e.g. instead of just navigating to a search page directly, we would have to navigate to the home page, fill in the form, submit the form, and then wait for the search page to load. These extra steps could also slow down our tests, especially if we need to repeat them for many tests.

Some requests only happen on the server, e.g. the API request to /me (to fetch the logged in user's profile data). With these tools it would be impossible to mock the responses for this subset of API requests.

Since we rely on SSR so heavily, it's very important that we are able to properly test the HTML response from our server. For example, we need to check that we're including the correct meta tags for SEO purposes. It's also very possible that a page could break for SSR yet still work for client-side navigations. For example, there might be a bug in our Express routing logic where URL parameters are incorrectly decoded. Therefore, so we need tests to help us catch bugs like this. It would not be possible to write these tests with a mocking tool that only works in the same process.

Decision

As we have seen, we need to be able to mock the request which happens on our server, rather than the request which happens in the browser. The only tools we could find that worked in this way were MockServer or Mockttp. Each tool has their own trade-offs and bugs.

MockServer:

  • It's written in Java so it's harder for us to understand the source code if we ever needed to.
  • It's not very well maintained. The author's last activity was in late 2020, and since then there have been lots of open issues without responses.
  • There is an option to define a "proxy" fallback for requests without mocks but this doesn't seem to work: mock-server/mockserver#839.
  • Annoying bug with when sending large response body: mock-server/mockserver#1036.

Mockttp:

  • It's written in JS so it's easy for us to understand the source code if we ever needed to.
  • It's well maintained and the main contributor is very responsive and helpful.
  • No option to define a fallback for requests without mocks: httptoolkit/mockttp#51
  • Not compatible with Cypress' best practices: httptoolkit/mockttp#52

For now we have settled on MockServer, but it should be fairly straightforward to migrate to Mockttp or a similar tool if we ever need to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment