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:
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.
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.
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 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.
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.