How to test a server-side rendered React website

How to test a server-side rendered React website

Testing a server-side rendered (SSR) website isn't that different from testing a single-page app (SPA) or a pre-rendered website (e.g., using Gatsby). What it comes down to is including the server-side code in the tests in some capacity. I will go through the testing pyramid below to get us on the same page with the different test definitions and then review what this means in a React SSR context. The short answer, however, is to use integration tests.

The testing pyramid

When it comes to writing software tests, traditionally, there are three levels you can use to target your tests. They are end-to-end (E2E) tests, integration tests, and unit tests. Each has its benefits and caveats. Behold, the testing pyramid:

Testing Pyramid

The argument goes that unit tests are cheap and fast, end-to-end tests are slow and expensive, and integration tests are somewhere in-between (as described by Martin Fowler). Therefore, you want to focus on unit tests mostly, then a medium amount of integration tests, and some end-to-end tests sprinkled on top. Kent C. Dodds argues that this may be less true now with improved developer tools. Dodds also introduces the "Testing Trophy" that shifts more focus on integration tests and also adds static tests (e.g., TypeScript, linting) to the bottom of the trophy. Dodds has a whole course for those wanting to learn more.

In any case, I tend to agree that writing tests until you reach 100% coverage is excessive. You want to ensure you have enough tests that you feel confident in your code. I've found that it's hard to argue for a certain percentage, and it's more of a gut feeling that comes with time and experience. Essentially, if you're not sure, you probably don't have enough tests.

A more critical requirement (besides accuracy) for tests is that they are fast. There are two reasons for this:

  1. You want to be able to run your tests all the time. If they are slow, you may not feel inclined to run them often enough.
  2. If the tests are fast, most likely, your UI is reasonably fast too. There is a lot more to performance optimization than this, of course. You want to avoid optimizing performance too early, but once your code is well-tested and does what you expect, refactoring the code until it's reasonably fast is an excellent next step.

But what does the test pyramid mean for React in a server-side rendered context? How do you approach writing tests for the different levels? Let's start from the top.

End-to-end tests

Also sometimes referred to as UI tests. Since we're already writing UI components, this might not be very clear, so I prefer to call them end-to-end tests. Essentially these are tests that should run in a real browser and, if applicable, use actual APIs and network calls. There may be exceptions to the latter where you, for example, don't want to trigger a payment and need to mock an API, but you want to avoid that as much as possible.

The main benefit of these kinds of tests is that they can cover much code in a single test. And not only will they cover the React SSR code, but they can also cover APIs whether they are first or third-party.

Here are some options to help you achieve this:

Integration tests

These are very similar to end-to-end tests in this context. Usually, the network stack is mocked entirely to avoid network latency. Integration tests may also include running the tests using a visual test runner (see above). A base requirement is that these tests involve connecting multiple React components together to test a full feature. You can either mount the entire component tree or just a section of it.

Integration tests are great because they can combine the best of both worlds. They can cover as much code as end-to-end tests in a single test but are more lenient when mocking third-party features. The main downside used to be that they're difficult to set up. Now I think the risk has more to do with performance and test speed.

In addition to the list for end-to-end tests, there are a few more libraries that can be helpful here:

Unit tests

For React, this would be a single component. To avoid needing to setup Redux or other providers, you'll want to avoid testing container-style components. If you do, it's no longer a unit test; it's an integration test. Writing unit tests is probably very uncommon purely for the benefit of testing SSR features, but Cypress seems to be adding support for this.

This type of test is excellent for getting into the nitty-gritty details of a single component. Of course, the downside is that you'll have to write many more tests to cover an extensive application or website.

The libraries and tools to use here match the ones under the integration tests above.

As you might have figured out, writing unit tests in React doesn't make sense most of the time unless you're writing a very complex component or creating a component library.

End-to-end tests seem reasonable, but requiring network calls and functioning APIs may not always be possible and can also slow down your test suite.

Instead, focusing on integration tests seem to be the best approach.

  1. It will cover more of your code in a single swoop (80/20 rule).
  2. It will run faster than end-to-end tests due to the mocked network calls.
  3. You can run it in a browser and simulate a whole user flow (e.g., sales funnel, authenticated content, network errors, etc.)

So what do you unit test in a React app? For me, it's mostly for utility functions and formatters that contain some non-trivial logic or a bug in a low-level component. For everything else, there are integration tests. You can use end-to-end tests for specific, one-off cases, but because they can be brittle and hard to get right, it might be best to avoid them until you need one.

What about SSR specifically? It's not that different from other forms of React websites. You want to ensure it renders the app correctly from a visual standpoint and functionally works as expected. The easiest way to do this is to rely on one of the visual testing libraries (Cypress, Playwright, etc.). This way, you'll get the rendered app from the server while also testing the frontend visually. Without rendering the React components server-side, you can't ensure you have avoided using document, for example, while running in the Node.js environment.

Stay tuned for a follow-up with an example of how I'd configure tests in an SSR app and refactor it once the tests are covering the main flows.

Cover photo by Anthony Tori.