3 testing tips we employ at Algoan for front-end testing

Charlie Smith
Algoan
Published in
6 min readJan 6, 2022

--

Illustration of Dashboard, a product by Algoan.

At Algoan, we provide tools to facilitate access to Open Banking for both end-users and our enterprise clients. Such is the importance of the reliability of these tools, ensuring their correct functioning is of paramount concern to the front-end team at Algoan. Our user flows must work correctly and be protected from any regressions that we may unintentionally cause during development of new features and refactoring. Manually retesting each user flow that we have already developed is simply not a practice that is scalable. As our applications grow, so does the quantity of features.

We use a mixture of React Testing Library and Cypress to simulate user behaviour and to test each facet of our web UIs. Our front-end tests should be thought of as a user-application contract. They are the specification upon which we build our applications. In other words, they form a series of acceptance criteria that must be validated in order to assert that we have a working application that conforms to the product owner’s vision.

A test should act as an aid, not a hindrance. If the test is hindering productivity whilst not providing sufficient value, it should be removed, or at least modified. We employ certain simple principles to make our tests easy to read and maintain, and also to ensure that they are useful in the first place!

Test what the user does and sees

Kent C. Dodds said it best:

“The more your tests resemble the way your software is used, the more confidence they can give you.”

Not so long ago, testing frameworks such as Enzyme were used to test individual components in an isolated manner. Such methods of testing rely on testing implementation details of components, such as props and state. This started to fall out of vogue when developers realised it made tests too brittle, as they can fail due to modifications of code internal to the component. On first reflection, this may not seem like such a bad thing if you only have a few components in your application. Alas, code soon accumulates, and before you know it, you spend more time baby sitting your broken tests than writing application code. A test shouldn’t be a burden that must be modified each time a change in implementation has been made.

As mentioned before, a test should act as a piece of specification. These specifications serve as the contract that your user-application must conform to, and the best way to realise such a contract is to test how the user uses your application. The user performs a series of steps, and as a result, they expect to see something on the screen. No testing of the internals of our React components, no testing of Redux state, no testing of props. Only the requests to the backend are mocked, and everything internal in the application is tested as it is meant to be.

When selecting elements, care should be taken to select elements in the same way that a user would. Below is an example of a Cypress selector which is responsible for finding an element on the page:

This selector attempts to find an element by its class name. This practice runs counter to our principle of ‘test what the user does’. A user doesn’t find a button on the page according to its class name. They can’t even see this information; it’s an implementation detail. The selector is highly subject to change, making the test brittle. It is also not usefully testing attributes of the button that are important, such as the text inside.

A better way of selecting an element is as follows:

Cypress looks for an element with a role of button on the page and the label ‘Delete’. It then clicks on it. We have aligned the way our test works with the way the user works, making the test more resilient to changes in implementation and also testing the label of the button in the process. It is worth noting that for elements that are tricky to select by roles, we also employ test IDs. Whilst these don’t perfectly reflect the method of interaction by the user, they do provide the same resilience to implementation changes.

Another example is how we assert on the desired behavior of our application. Instead of asserting that the internal state of the application has the correct values, we simply assert on elements that are on the screen. After all, this should accurately reflect the internal state.

The above assertions are performed after a deletion operation. Instead of checking the Redux state to see if the front-end state has correctly removed the deleted element, we simply check if everything that should represent a successful operation is shown on screen. That is to say: a successful notification appears, the list of banks only has a length of 1, and the organization that we have just deleted (‘our test bank’) no longer exists on the page.

Cypress and React Testing Library heavily dissuade users of their framework from testing applications in ways contrary to these practices, so much so that inspecting internal props, internal state, and manually setting state values are things that are simply not possible using these frameworks.

Choose a good name

A test should be precise, and everything starts with the name. Deciding on a strict, clear and concise name for the test means you limit the scope of what is being tested. By devising a good name before you write your test, you force yourself to be precise in what you are testing.

These names are setting up your tests to do too much. If these tests fail, it will be hard to identify exactly where the point of failure lies. Is it the deletion of the project that has failed, or rather the addition of another project? Furthermore, the test will be too long, which muddies its readability.

Here, it is clear what is being tested. Each specification is independent of the other, and dependencies between user flows are limited. When the test fails, whether that be in the CI or locally in your IDE, the comprehension of the point of failure is immediate.

Tell a story

Any good test should tell a story. The name of the test is the chapter title, and the body of the test is the content of the tale.

A test should try to read as close to plain English as possible. Don’t make it too unwieldy and long. Use abstractions where necessary to hide arduous boilerplate. Avoid using selectors that are difficult to read and prone to error, such as CSS selectors. Don’t be afraid to use comments. Your test is ultimately a series of steps that a user takes, so making them easy-to-read provides your application with a good level of developer documentation for free.

Below is an example of one of our tests which tests the creation of a project:

Even if the reader does not understand the business logic, the test is comprehensible and tells a story. We take advantage of Cypress’s tools to create abstractions for commands such as logging in and navigating to the correct page. In the same vein as abstracting code to named functions to make application code more readable, Cypress allows us to do the same for statements that are often reused across tests. Furthermore, it allows us to assign names to these reusable commands so that the test reads like plain English.

It is best to abstract away selecting elements by their index and keep the upper level of the test free from anything that lacks description. By doing this, we can turn those two commands into an action that is immediately clear to the reader and portrays user intent:

Another thing to note is that comments can be utilised throughout the testing steps if you think they are necessary. Ideally, your functions names and selectors should tell the story, but don’t be afraid to add a comment if you don’t think the steps are clear. Your tests serve as a form of documentation for the developer, and being over descriptive is not a bad thing.

Conclusion

Testing is a complicated beast. There is a wide array of opinions and best practices on how to write precise, maintainable and useful tests. Front-end testing is constantly evolving, and it is still in its relative infancy. Some principles such as avoiding testing the implementation of components have only come about in the past few years. At Algoan, we put a great deal of focus on trying to keep up to date with the latest in front-end testing practices in the industry. It’s great to share some of the methods that we use, and we hope to be back soon to discuss other forms of front-end testing that we employ! :)

Find out more about Algoan on www.algoan.com.

--

--