Client-side testing tools

adi carmel
Bootcamp
Published in
6 min readApr 27, 2022

As full stack developers, we need to write tests for both server and client code.

Test tools and techniques for server-side applications are generally more mature, while client-side testing is significantly more challenging. Most of the time, testing individual components, simulating user actions, or testing CSS changes can be difficult.

For the last three years, I have worked at tomorrow.io and developed hundreds of UI components for our B2B product. Meanwhile, client testing tools improved significantly, and today we have very good tools and techniques to test client-side applications.

I would like to share with you our client side testing approach, what we test, and how it is tested.

Why UI tests are important?

If you work alone as a single developer and build a small app that contains only a few pages, test is easy when everything works as expected. It is also quite easy to perform regression tests. After a new release, you can easily see what has been changed, and if anything has been broken in the new version.

When your team grows and your app contains many pages, with many variations, it becomes almost impossible to check if the latest release will not break the app. Furthermore, you are typically unaware of all the other team commits, so you do not know if your new version will break theirs.

At tomorrow.io, we have a few teams working on the same app. In addition, we have a feature flag mechanism, where we release features that are restricted to some certified users (admins, users from a certain industry, users who have paid for a custom feature, etc.). Therefore, it’s very difficult to know in advance when a new version will break some functionality, for some of the users, after merge.

How do we split our UI application code?

First of all, we always prefer small, reusable components. We build a library of core components (buttons, dropdowns, lists, etc.) and build our complex components on top of them.

Each component has its own storybook file that contains all of its variations (button small, button primary, button secondary and small, and so on). We move all the logic into stand alone helper files, and move most of the react hooks into custom hooks. This way our components code is very clear, and handle only the UI itself.
This separation helps us using different test methods for each type of code.

Business logic code

Because our logic is isolated as pure functions inside the helper files, we can test it with regular unit tests (we use jest for it), without involving the UI at all.
As those tests are very easy to implement and run very quickly, we use them on each helper file function.

for example: we isolate the filter functionality out of the UI and test it separately with simple unit tests

pure function are easy to test isolated with unit test
the test results can be used as the function documentation by other developers

Configuration data

Due to the product’s complexity, the configurations data is often very long, making it very difficult to test new updates. In this case, we use jest snapshots as testing tools.
We aim to limit these kinds of tests on non-configure data, so we only have a few of them.

for example: our chart can be initialized with a configuration file that contains more than 100 lines. With a snapshot, you can see just your updates, and decide if they’re desirable or not.

configurations data is easier to test with snapshots
If configuration data is large, it’s easy to keep track of changes by using snapshots

UI rendering code

For basic rendering tests, we use React Testing Library (RTL). Using RTL, we can render the isolated component, pass some properties into it, and expect to see text (or roles) as a result. By doing so, we can quickly and easily test the basic functionality of the components, and ensure that future code modifications will not break them.
We have a lot of those tests, because they run quickly and are very easy to implement.

for example: when the <TopBarActions /> component gets the showCollapseExpandButtons prop, it should render the text Collapse and Expand into the UI

basic rendering tests with RTL — we pass the relevant prop and expect text to appear in the UI

UI interactions

The interaction of our dynamic components is tested using storybook interaction tests. Whenever we want to test how our UI behaves when someone is using it, we simulate some actions and inspect the component to see if it renders the right output based on those actions.
As these tests are harder to implement, we have fewer of them, and we usually use them on components with heavy user interaction, where we want to ensure that everything is working properly.

for example: render a search component, type a string into it, then make sure this string appear in the component

“search-input” storybook interaction test
storybook interaction can test user input

Visual snapshots

While all the above testing tools are being used, some UI bugs may still get into the production code. In most cases, it will be visual changes caused by changes in padding and margin, or because of changes in the relationships between components.
In order to detect those changes that are difficult to identify using other tools, we need to use a visual regression tool. we use chromatic visual testing to render our components (with all their variants). Chromatic takes an image for each component (and variant), then for every pull request we wish to merge — It will take a second image, and compare them.
Changes will be asked to be corrected or approved before merging the code. Using Chromatic for those kind of tests is very easy since their native integration with Storybook makes it very straightforward.
These tests are relatively easy to develop, so we have a lot of them, and we use them for every component we can (the only downside is their price..)..

for example: when we change the padding in our button/medium component, we did not notice that we broke our split-button/icon. because we use visual snapshots tests, we found it before we merged the change

As a result of the “button-medium” padding changes, the “split-button” icon shrunk

E2E tests

After all those components have been tested, we still want another, higher level of testing. This is done with end-to-end testing.
Puppeteer is used for testing more complex flows on the system.
These tests are much slower and harder to write, which is why we only have a few of them that cover the most common flows in the system.

for example: Test that the user registers, signs in, and then sees the homepage.

In the most common flows in the system we use e2e tests

Summary

All of us want a well-tested code so that we can sleep peacefully at night without worrying about our latest version which might break the customer experiences. If you apply the methods mentioned above you might be much more confident with your code changes, and (hopefully) sleep better.

--

--