Testing Next.js website with Cypress

Идель Гизатуллин
Идель Гизатуллин · 5 сентября 2021
Frontend

The Problem

When you write Cypress tests for Next.js apps, you can’t mock requests coming from SSR. You can only mock requests coming from the browser via cy.intercept().

The Solution

Spawn your Next.js server as a child process of Cypress test runner and mock all HTTP communication with Nock.

Gleb Bahmutov described this method in great detail in his article Mock Network When Using Next.js getServerSideProps Call . And you can also watch it as a video:

The Story. What is Next.js?

React.js is like a CPU that is central to every PC, but not really useful on its own. Next.js is like a PC that uses React.js as a central piece but is much more suitable for empowering people in solving their problems

React.js is just a UI rendering library and you will need a lot of infrastructure around it to build a full-featured website. You may use Create React App that has a lot of good stuff built-in, but Next.js is a lot more than that.

If your React website needs to perform well on slow devices and be SEO-friendly, then Next.js is a great choice. But this comes at a cost: you will need to maintain a Node.js server for your frontend.

Note: Next.js is pretty flexible in a way that you can opt-out of SSR and pre-render your pages at build time into static HTML or make some pages render client-side only to reduce the load on your Node.js server.

Why would you test your Next.js website?

Chances are you’ll want to use your Next.js app for a public website with a wide audience of users. How do you ensure your big new features positively impact your users’ engagement? We have a practice of running experiments on some percentage of users before we do a full rollout.

Experimenting via A/B testing is where your website’s code can become pretty complex. Not in the sense that you need a maths degree to understand what’s going on. But there are just too much possible variants of logic branches that you should keep in mind and maintain every time you make changes into the codebase: some experiments are running for users in a specific geography, some are randomly picked and there are a lot of permutations among them.

Evan’s Awesome A/B Tools

In our case, we had several separate teams who would change some part of a website for a percentage of users. This means that we had multiple versions of a website living in the same codebase.

Some people write tests because they are enthusiasts. But this is the case where you have no choice but to start writing tests for your frontend code because it’s not feasible to test it by hand on every release.

Writing a unit test is pretty easy. Especially if you utilize jest’s mocking abilities . But unit tests won’t give you too much confidence that your website works as expected in every way that myriads of different users experience it.

Introducing Cypress

Cypress is an end-to-end testing framework with great developer experience. It has almost anything you’ll need to test your website. You should check out Cypress website . Did I already mention Gleb Bahmutov’s blog , where he’s on a path to describe a way of testing every niche case on the modern web?

So let’s try and test our Next.js website with Cypress. How Cypress is different from Jest? Jest is a test runner that will take your code as an input. But Cypress will take a URL with a running website as an input. Cypress will open your website inside Cypress Browser that has special features for testing. One of these features is cy.intercept() which gives you the ability to control the Network layer of your website’s javascript code.

Every test usually consists of three phases: Arrange, Act and Assert. If you use a real backend in your tests, your Arrange phase will make your tests slow and flaky, because you will need to imitate a lot of different cases in your testing database. You will need to maintain code that transitions your database into the state that is relevant for every case that your frontend supports.

This is why I think that cy.intercept() is so cool: you can just save backend responses relevant for every test as a static fixture and run your frontend logic against them. And this is why I got frustrated when I found out that Cypress can’t control Network on Node.js processes where my code was running.

How I thought it would work: use MSW to intercept requests coming from SSR

MSW is a library that can control the Network layer for testing purposes. In browsers, it uses Service Worker API , hence the name Mock Service Worker. And a little blue bird was spreading a rumor that MSW can now mock requests from Node.js servers!

But here is a catch: once we define fixtures for our mocked backend API and start running our tests, we can’t change them. But we certainly want to change them: the whole point of testing is that we can Arrange different preconditions for every test and Assert that our code Acted as we expected.

Let’s explore this problem visually:

MSW has worker.use() API to change mocked responses. But we can’t call it from Cypress test, because it runs in a separate process. We need an IPC.

This is how I thought it would finally work:

So now I had to write some code to get IPC up and running. But I was procrastinating and stumbled upon a tweet:

https://twitter.com/delba_oliveira/­status/1427307679702192129

So I’ve found a relevant thread where I described my problem.

And this is where a developer from Central America told me about another developer who already solved my problem. And he did it just a week before I’ve started my research.

The end

Every time I face a complex technical problem, I have a feeling that somebody should have had it already. I try hard not to solve it by myself and find like-minded people because I believe that building software gets fascinating when we do it together.

And I have come to realize that it’s crucial to have the skill of describing technical problems which I try to develop right now.