Front End Testing: General Guidelines
- Dale Fukami

- Oct 30, 2024
- 6 min read
Updated: Nov 7, 2024
Recently we were asked for our thoughts on how we approach front end testing. This is, of course, a vast topic that's difficult to cover completely but this series will demonstrate some of the ideas and principles that guide our decisions when testing on the front end. We'll be using React in our examples as this is where most of our experience applies but the underlying fundamentals of why we choose certain tactics should apply in most front end environments.
Qualities Of Good Tests
This is the second in the series and covers some of the general qualities that we look for when writing our tests. These qualities aren't specific to front end testing, but we'll look at them from the lens of a front end project.
Here are some things I strive for in my tests:
Isolation
Easy to write/modify
Easy to read/scan
Test "one thing"
Fast
We'll be starting off by continuing our exploration of the todo list project shown in the first post. Our starting point can be found here. As we look at each item we'll refactor the tests to demonstrate some of the ways we achieve the desired qualities.
Quality 1: Isolation
When I talk about isolation in front end tests I'm referring to a couple of things:
Isolation from external data/setup dependencies: I shouldn't need any global data setup or configuration in order for my test to run properly. For example: I shouldn't expect data dependencies of some other component to have to be configured in order for an unrelated component test to run.
Isolation from external components: A change in the implementation decisions of a subcomponent shouldn't force me to change the tests for a parent component unless the interface (properties) has changed.
Looking at our existing tests for the Task component we can see that all the necessary data required can be controlled from the test independently as they're just props. In addition, there are no subcomponents to speak of aside from the HTML span element which we'll gamble on the behavior remaining the same. We'll return to this item later when we start addressing the level of our tests (integrated vs unit).
Quality 2: Easy to write/modify
How easy is it to write new tests for our Task component? Opinions will vary, of course. Some will say it's pretty easy at the moment as you can follow the template of the existing tests and modify as needed. While true, I think there's some work that can be done to make them even easier to write. The main issue I have with them at the moment is that every time a new test is written I have to fill in all the properties required to render a valid component. I try to write components that don't take a lot of properties but even at just 4 properties this slows down my ability to dive into a new test quickly. Let's do some refactoring to take care of that:
(View the whole file here)
I've done 2 things here.
Extract a subject helper function that provides sane defaults. This makes writing new tests simpler as I can just pass in the properties that I know actually impact my test.
Extract a textNode helper that allows me to get the text element without having to understand exactly how to find it. This allows me to fetch it with the text I know I set in my specific test or just let the tests find it by default values.
Quality 3: Easy to read/scan
There are a few things that I find make it easier to read or scan a test and understand what's going on.
Impactful setup is easy to identify. We achieved this with the method extractions in the previous section. Now our setup step makes it very clear which properties of the component are important preconditions for the test.
Impactful actions are easy to identify. When reading a test it's important to be able to see at a glance "what action causes our desired post-condition". For some tests there is no action, such as the tests that just verify rendering.
Often reads like a manual test script. It's far quicker to understand a front end test if you can read it in the words of a user or as you'd explain to a manual tester.
Let's tackle item 2:

(View the whole file here)
I've removed the spacing in the tests with no action. This groups the "assert" section together. When I see a test with only 2 distinct blocks I assume there's no "act" section as the act was just rendering the component. Following the "arrange/act/assert" convention makes scanning tests very easy as long as each section is fairly trivial.
And now item 3:
(View the whole file here)
I know that a manual tester doesn't view an individual task in quite the same way we do in a test like this, but we can imagine describing to someone, "find the task and click on the text". The tests now read more like that but within the isolated context. At this point I'm pretty happy with how these tests read.
Side track: While writing this post I had an idea that I wanted to experiment with that might make scanning the event based tests a little nicer. Here's what the event publish tests look like in the "arrange" section:
const onIncomplete= vi.fn();
subject({ isComplete: true, onIncomplete });While two lines in the setup isn't a lot to read I wondered if there might be a one line version might work:
(View the whole file here)
I've never tried this before, but I think I quite like it. The properties that impact how the component should behave are in the subject call and I have access to the default mock functions on return. This takes a bit of a leap of faith that my subject functions behave a certain way but I'm ok with that. In every project there are conventions that build up and I think I'm willing to stick with this and see what downsides crop up.
Quality 4: Test "one thing"
This is a bit of a fuzzy one and I'm sure you'll find me contradict myself in examples all over the place. The intent here is that each test only verifies one concept. This is fairly easy to adhere to when the components are quite small and I think the existing tests follow this rule. However, here are some alternatives to explore.
Single Assertion
One way to interpret this quality is that each test should only have a single assertion. Currently we have some tests with multiple expect statements. Let's split them into independent tests:
You can see that I've split the tests for the css class into independent ones to verify a) the right class is there and b) the wrong class is not. This now very strongly follows the "test one thing" concept. Is it valuable compared to the initial version? Use your judgement. IMO, this is too extreme. I prefer the original version as a test of, "has only the correct class" as the "one thing" being tested. 2 asserts may be required to verify that "thing".
Single Piece of State
Having said that, maybe "rendering the correct css class" is actually the "one thing" we want to verify. Let's try this:
In this case we're saying that there's a css class that identifies the state and we're testing the "one thing" that it is correct in the various states. Again, I think this comes down to team preference. I thought I'd be ok with this when I started out but now that I see it in front of me, I prefer the original. This takes me too long to figure out that there are 2 sets of states being tested. For me, the 'cleanup' in the middle of the test is an indication that maybe it's stretching the concept of 'one thing' a bit far.
Quality 5: Fast
I like my tests fast. I run my tests constantly. As someone who generally uses TDD during my development work I need very rapid feedback. Now this doesn't mean I don't have some slow tests in my systems. It just means that, in general, my tests are fast. As fast as possible. If I can make a trade off that favors speed of the tests I'll consider it very seriously. I think individual tests should run on the order of 10ms or fewer and the entire "unit suite" should be below 30s. I expect this suite to provide me with 95% confidence (for some value of 95) in my product. The remaining tests may be a slower suite that exercise various integrations, etc, and take longer to run and I'll rarely run those locally.
Conclusion
These are some of the qualities we find valuable in our tests and some things I'll do to try to achieve them. As you saw, I'm constantly trying new things and evaluating different ways to achieve the things I've found to be successful. You may disagree with some of them or with my style. The important thing to take away is to know how you, as an individual and a team, want to approach your tests. What's important to your test suite? What properties give you confidence in your system without over powering their value with huge costs of writing/maintaining/running them?

Comments