Mocking in Cypress with NextJS

TL;DR
The article explains how to implement a mock backend for frontend testing in a NextJS application using Cypress. To ensure consistent and efficient testing, the team explored options like deploying a separate backend instance or using Docker, but both had drawbacks. The final solution was to create a minimal backend in JavaScript within the Cypress setup, allowing tests to intercept frontend requests and return predefined responses. This approach ensures tests are easy to run on local machines and within CI/CD pipelines.

In order to verify the correct functionality of the individual parts of the system when implementing Knížkomat, it was necessary to test our code. For the backend we chose the integration test approach, which verified the correct functionality of each endpoint, but this article will not be about them. On the frontend, we decided to use end-to-end UI tests that run selected scenarios within the application as if it were a normal user. For these frontend tests, we had a few basic requirements:

  1. must be part of a frontend project and written in a language that frontend developers know
  2. must be executable within the CICD that deploys the application to the server
  3. it must be easy to run on the programmers' development machines

The Cypress library meets all these requirements as it allows you to write tests in JavaScript, tests can be run via NodeJS and it works very easily on any device. In order to keep the tests independent of the backend and to make it easy to inspect the data the test is working with, we decided to mock the requests coming from the frontend in some way and return prepared responses. However, here we ran into the main issue with our whole plan and that is the middleware written in NextJS.

Problem Formulation

In the main article about the Knížkomat project we mentioned that middleware, above other things, makes it easier to create requests on the frontend. This is true, but since the middleware has control of the communication with the frontend, this communication is not as clearly defined as the communication between the middleware and the backend, which is based on the REST API defined on the backend. So if we want to mock requests in Cypress, we need to intercept messages without prior knowledge of what those requests actually look like. While this is not an unrealistic task, it is definitely very challenging, as we need to capture all the messages for each request and then manually "decipher" what each message means, what format it is in, and what response it expects. All this when just one step further (i.e. between middleware and backend) we have a beautifully defined and human readable communication.

Possible Solutions

The first solution we came up with was to "wrap" the middleware into the Cypress process so that it could then intercept communication to the backend. However, since the middleware is a separate process, Cypress wants nothing to do with it and although it runs it as part of a test, it is not possible to wrap it in mocking functions. So we thought of a few alternative solutions. 

One option would be to deploy a separate instance of the backend that would be used only for testing purposes. The problem with this approach is that the backend would be persistent, and if a test modifies the data on the backend, the next time the test is run, the data would already be modified, which violates the principle of test consistency. Even if the data consistency was somehow resolved, there would be a full-fledged instance of the backend running on the server that would do nothing at all most of the time, which is not very efficient.

Another possible solution is to run the backend in some form of virtualization, most likely Docker. This would allow us to create and terminate backend instances easily and "cleanly" and would not break the consistency of the tests. Within CICD, this approach could be used, but Docker breaks the third requirement mentioned in the introduction, as some of us have development machines running on Windows, which complicates the ability to integrate Docker into tests. This is not an insurmountable problem, rather an annoying one.

The third option was to create a minimal version of the backend that could be easily run on both development machines and CICD. This minimal version would only be created for use in tests and could be written in Python, for example, to make it easy to understand and fast to run. The only downside is the need to run the Cypress tests and backend separately.

Our Solution

Our final solution is based on the realization that a minimal version of the backend can be written directly in JavaScript and that it can then be run by Cypress within the setupNodeEvents configuration. So we created a backend on the frontend via a single file that acts like a classic server, i.e. it listens on a selected port and responds to selected requests with predefined responses. This way, we don't mind that the middleware cannot be mocked, because the whole server that the middleware queries is set up exactly for the needs of the tests. The advantage of this approach is that the whole process is triggered by the same command as the tests themselves, so it can be run with a single click on the development machine or with a single command within CICD.

frontend

NoxLabs is a team of engineers and designers specializing in web and mobile development. We're passionate about building beautiful software and welcome new project ideas.

Want to work with us? Drop us a message