Opinion: SSR Front-End Unit Testing, Not Worth It
Highlighting the issues developers face in production and how to solve them.
It happened, that throughout my career as a front-end developer, I have never worked on a project where tests were a priority, in fact, they were never mandatory.
The workflow would generally include planning, development, QA, owner’s check, and release. I am not saying the result was bug-free 100% of the time, yet the issues had little or nothing to do with development. Meaning, that either in the stage of planning, we did not anticipate users’ needs, or after release the end user would give negative feedback on a feature as a whole. Either way, what we needed the most was to ensure continuous delivery at the highest speed possible.
In such a setting we might have looked something like this:
Summary
First I highlight the problem, which is the frontend unit tests from docs are useless and tell you nothing while being costly. Then I tell what issues we actually face in production and those are not rendering of what we want or rendering something not how we want it. As a result, I suggest how can we address real problems and that would be a simple if statement. Closer to the end I discuss ways to improve THE BAD and ways to improve the UGLY of the suggested solution. I conclude the article with some almost visionary improvements.
Disclaimer
All you read below might not apply to your case, but unless you work with a FAANG-tier legacy project, where a product card may have over a hundred states in a proprietary design system or a really complicated business logic on frontend instead of backend, I strongly advise you to have a look or share it with a frontend dev you know.
The problem
Hiring manager: So, among all React weaknesses, you mentioned documentation, why is that?
Candidate: Well, if they have something a little less common like the useRef() hook, they usually say not to use it, or WHEN not to use it. What they always lack is the appropriate use cases of their API.
Well, to be completely fair, they kinda extended the docs quite a bit, for the useRef()
at least, but I completely agree with this concept. For every piece of docs I meet (especially in React), there are lots of DON’Ts and not enough DOs. The same thing goes for any frontend testing examples, all they are showing is some “hello, world”
of testing, which consists of checking whether a component is rendered or not.
Which brings us to the core of the discussion. What do we need to test in a front-end app? To be even more particular, what do we test in an SSR Gatsby website with no backend but with a Storyblok CMS and CI/CD in Bitbucket, which prevents deployment if the build is failing?
What the tech supervisor usually wanted from me were the unit tests, because they are the most “popular” and “manageable”. The idea behind this is that you develop and component and write tests for it in isolation, making sure it works and behaves correctly. But what does a front-end unit test show us? Let’s have a look at the official docs and some related articles.
What do professionals write about testing in React?
React docs (Testing Overview — the testing section lives only in legacy docs):
- Rendering component trees
- Rendering a complete app
Jest docs
React test recipes (legacy docs only)
To sum it up, an article at FreeCodeCamp How to Write Unit Tests in React does an awesome job by splitting tests into the following categories:
- If a component renders with or without props
- How a component renders with state changes
- How a component reacts to user interactions
At this point, I really started to have questions, since even before reading the article I was outlining pretty much the same categories where we are trying to answer the following:
- Component IS RENDERED vs. IS NOT RENDERED → depending on props
- Component IS AFFECTED vs. IS NOT AFFECTED → depending on state changes
- Component IS INTERACTIVE vs. IS NOT INTERACTIVE → depending on user actions
I am completely lost in each of those scenarios. Maybe, there are so complex and overloaded apps, where you cannot answer at least one of those questions during development, but I am lucky enough not to be stuck at any of those questions on localhost
. Hence, I ask, why we even need the unit tests, if they answer nothing, which is important. Let’s address my current project and try to identify what might be useful to check
What issues do we actually face in production?
As mentioned above, I’d fancy some real-world examples in the docs, with the lack of those I’ll try to categorize problems we encountered on the website.
Component does not get some props we need and renders anyway.
- Details: let’s say we have a
HeroSection.tsx
, with atitle
field and this component has some vertical margins. It does not crash the project, when nothing comes from the CMS. - Result: some whitespace is rendered with no text.
- Implication: worse user experience, ugly look.
Component does not get the props we need and the build crashes.
- Details: imagine, you expect some
items
, with aname
and aprice
without any checks (even without conditional chaining!) you justitems.map()
them. - Result: build crashes in the pipeline, nothing changes in production.
- Implication: bad DevEx, slow feature delivery.
Essentially if we struggle with some problems, they fall into the “RENDERED vs. NOT RENDERED” range. It has never occurred to me to release something that does not respond to state changes or user actions. If you develop something like this and your QA is not able to identify it, probably tests won’t help you, and neither will this article, to be honest. But then again, you might have an extremely complicated constellation of micro-frontends with no design kit on top of that, and in this case…
How can we address real problems?
The first thing people tend to suggest is: “just write unit tests!” Well, — “no”.
There are a few layers to this “no”:
We sacrifice development speed approximately twofold or more. Just look at any of the official examples in the docs one more time
- 8 lines of component vs 30 lines of test Testing Recipes — React
- 24 lines of component vs 18 lines of test Testing React Apps · Jest
We have a really hard time mocking Storyblok data structures.
richtext
, which is heavily used in CMS. Furthermore, even if we manage to reverse engineer the Storyblok’s rich text, it immediately becomes a tightly coupled legacy. This pyramid of spans might break with any update of theirs or we can just move to another CMS with a different approach.links
, which may be internal and have “nested” routes or external with normal URLs, so we have to mock 2 very different structures to test, not knowing which one we are getting.assets
like files and images. For instance,.svg
. We might have design reviews once in a while, where they try to uniform some bullet points (where we accesspath -> fill: $color
) or decrease overall.png
presence and the tests become outdated.
And the most important thing
A passing test with props and let’s say failing without them solves essentially nothing. Since we develop templates for pages, they should be as reusable as possible, and making all props required will just break any flexibility for us and content writers.
But fortunately, there is a very “cheap” way to approach the “RENDERED vs. NOT RENDERED” dichotomy. All you need is an if statement with “required” props for every component.
So if you really expect something from the CMS and do not want your component to be rendered at all, just return null. And this is just the tip of the iceberg.
Is the if statement really enough?
I might feel repetitive by this moment but “no”. This time actually “no, but it does the job”.
Let’s see the implications:
The good
- It is “cheaper” and much more concise than a real unit test
- We do not render some weird-looking component (and depending on your CSS approach even its whitespace might never be shown)
The bad
- It is clumsy and repetitive
- One cannot ensure, all components really have it (hold this thought!)
The ugly
- When a component is not rendered, you have no chance to say if it is OK or not. Let’s say a content writer forgets a buttonLabel and boom, the component is gone, the overall look is decent, but we do not have a part of the page and we cannot debug it website-wide for tens of pages translated to 5 languages. But hey, it is exactly the same with the unit tests!
Ways to improve THE BAD
Go DRY
Let’s start with the DRY principle, to make it reusable. We need to write a function, which:
- accepts an object-based generic, since we do not know what props and how many of them we will pass
- iterates over each field value
- checks if those values are all acceptable, this is something you might need to update if you fail to anticipate all possible data structures
- returns false if some of the values do not satisfy our “required” idea
- returns true if all fields/props are “filled”
My realization would look something like this:
Please take a closer look at array checks, for images and Storyblok types we would certainly need to extend the if statement, but the direction should be clear. We can improve it even further, by passing the component name as the second parameter and logging the missing field names, grouped by component name in the development environment, but I prefer to keep it simple for now.
Now we can have an easy and consistent if statement in every component:
What if the team ignores it?
To ensure people actually use it, we finally need tests, which make sense. I suggest using jest.spyOn(object, methodName)
The Jest Object because we do not need to test the implementation, but ensure the method is called in every component.
Ways to improve the UGLY
Well, if everything is done right, we have a setup, where we do not render components, which are lacking “required” fields from CMS. But as mentioned the flexibility of some components and CMS in general makes it possible to overlook those “required” fields and you just end up without a component on a page. Everything is working, but a pricing page without pricing plans does not make much sense. The plot thickens when we are talking about tens of pages and different departments working on localization for 5 languages.
The solution I suggest is far from brilliant, but it works as well as the if statement for each component. The mechanics would be as follows:
- during
onCreatePage()
cycle check every field coming from Storyblok - check each field for empty and save results to an object with both “filled” and “empty” values
- create a simple debug page with filters by page and a possibility to toggle “all” / “empty” values
Since the realization of this mechanism is way too big, I will only showcase the CLI version of this debugger:
Now content writers have a clear vision of what is missing and whether it was on purpose. Then they just go to Storyblok and fix those fields if needed.
Conclusion
Thanks for getting this far! I do hope that I managed if not to convince you that mindless unit tests for the frontend are bad, but at least make you wonder if there is a better way to approach testing of SSR-generated websites.
If we think of what comes next, I would try to automate the debugger. We need to set up a diff system that reports content changes to a corporate slack channel, making accent on fields that become “empty” since the last build. Ideally, it should create a report for the last published page in the CMS, highlighting the empty fields on it.
Please share and repost, I would appreciate any critique and discussion of my unorthodox approach to the problem of testing frontend. And never forget, everything you do must make some sense, you are not allowed to follow “best practices” just because they are considered “best”, especially if they are the unit tests for a frontend app.
In Plain English 🚀
Thank you for being a part of the In Plain English community! Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Discord | Newsletter
- Visit our other platforms: Stackademic | CoFeed | Venture | Cubed
- More content at PlainEnglish.io