How to Write Better UI Component Tests With Testing Library
Tests that mimic user behavior help us create better software products
Testing is an important part of software development. Although testing is still sometimes regarded as an afterthought, especially in time-critical projects, methodologies like Test-driven development (TDD) and improved tooling for testing have done a lot in recent years to rectify this.
There are great testing frameworks out there for writing unit tests, component tests, and end-to-end tests in web projects. For UI testing end-to-end testing frameworks like Cypress or Selenium are often used. As with all things, there are good and bad things about end-to-end tests:
- Initially, end-to-end tests seem easy to write. However, it’s easy to write brittle end-to-end tests that have to be adapted whenever there are UI changes.
- End-to-end tests typically require a real browser like Chrome to run. The advantage is that you can visually see the UI interaction in the test. On the flip side, these tests usually take longer as downloading a browser, loading the webpage and interacting with the UI take time.
- If end-to-end tests rely on HTTP calls to services and third-party APIs you have to deal with network issues, tighter coupling to other services and flakiness.
You may be familiar with the famous testing pyramid. It roughly shows us the division of effort we should aim for when writing software tests.
Why Don’t We Have More Component Tests in Frontend Projects?
At the company I work for we strive to have solid test coverage. Regardless of the programming language or project scope we want to ensure that testing is in place early. We do this by setting up test coverage thresholds and a testing framework to facilitate writing of tests.
My favorite framework for creating web applications is Angular, which already brings a solid testing foundation. To improve the developer experience we switched to Jest which is a versatile JavaScript testing framework. Jest helps us to write tests that run fast and we can easily mock and stub dependencies (e.g. third-party libraries). Thanks to Jest, TypeScript and Angular’s testing API we kept adding more unit tests by default, while at the same time reducing the number of end-to-end tests.
However, I felt that some developers were still hesitant to write tests for Angular components and directives. Components are the building blocks of creating modern web applications and static websites. Yet, we don’t often write tests for something that is core to our projects.
- I have often seen component tests where the component is instantiated like a plain class, not a real Angular component.
- In many of these component tests, user interaction doesn’t reflect reality — events are simulated or functions are invoked without any user interaction.
- Sometimes, developers just deem it to not be important to write tests for components. I remember some conversations where the answer to why a component test was missing was something like, “It’s just a select component, what’s there to test?”
- I was also not satisfied with the way you would read these tests to understand what’s being tested.
The Angular CLI creates a basic unit test by default when generating code like a new component or a new directive. Here’s a simple example that is testing that WelcomeComponent
outputs the right text in a <p>
element after clicking a button.
My personal issues with tests that look like the example above:
- The test setup is not super intuitive. If you know Angular even a little then you probably understand that you need to provide the dependencies, such as imports of Angular modules. Still, the code looks more complex than it should.
- Change detection in unit tests is often a source of confusion, especially for beginners. By default, Angular won’t do change detection as we are used when running the application. When updating a property or simulating an event we need to let Angular pick up these changes or they won’t be reflected in the DOM. Besides, components using
OnPush
change detection aren’t easily testable (although there’s a good workaround for this). - The tests are sometimes not easy to read. While you can give your test cases meaningful titles I have often come across test code where I wouldn’t know what was being tested if I didn’t read the title of the test case.
- The test setup doesn’t provide a way to initially set up component properties without reaching down to the
componentInstance
. - If you want to write a proper test for an Angular directive you need to create a dummy wrapper component that uses the directive you actually want to test.
- Any event (e.g. by
@Output
properties) can be simulated usingtriggerEventHandler
. This is useful for events that are hard to replicate in unit tests (e.g. infinite scroll). If we use this too much we have tests that might not be accurate, though. Example: Suppose there’s a test for a component that would call a function when a child component emits an event. This test could work even if the child component does not emit the event itself when we’re simulating the event usingtriggerEventHandler
. - For finding DOM elements you can either use Angular testing API (e.g.
query(By.Css('a-css-selector'))
or plain JavaScript (e.g.document.querySelector('a-css-selector')
. While this is fairly easy, relying on CSS selectors leads to tests that can easily break. Besides, a user won’t find elements on a webpage by a CSS either so we ideally don’t do that either. A user would for example want to get a form field which is described by a label with the textxyz
.
When I found the tweet below in my timeline it gave me the idea to look for a solution that could be easily integrated into new and existing projects.
This is when I stumbled over Testing Library. Let’s see what Testing Library brings to the table.
How to Write Component Tests with Testing Library
Testing Library is a project started by Kent Dodds. It started as a React-only project. Today, it supports a variety of additional frameworks like Angular, Cypress, Vue.js, and more, as well as plain JavaScript. It’s neither a test runner like Jest nor tied to a specific test framework, which makes it easy to integrate the Testing Library in new or existing projects.
Testing Library is built on the principle that the more your tests resemble the way your software is used, the more confidence they can give you:
- Tests only break when your app breaks, not implementation details
- Interact with your app the same way as your users
- Built-in selectors find elements the way users do to help you write inclusive code
Sounds promising? I think Testing Library delivers on its promise. Here’s an example of an Angular component test that uses Angular Testing Library building on top of Testing Library. The component being tested is a select component which contains tags as selection options.
Let’s have a look at the difference to how a typical Angular component test looks like:
- We are calling a
render
function to create the component we want to test. The Angular Testing Library will invoke the Angular testing API under the hood (see how you can specify options likedeclarations
andimports
?) but we can do more like initializing component properties. - Instead of simulating events, we try to mimic user behavior as closely as possible by interacting with the UI. In one test case, we first press the tab key to move the focus to the select component, then press the ENTER key to open it, and finally click the option which contains the text
Silver
. This has the nice side effect that the code is much more readable in my opinion as the Testing Library API is rather expressive. - We aren’t doing any manual change detection as the Angular Testing Library takes care of it when rendering a component or performing user interactions.
- For finding DOM elements we mostly avoid querying by CSS. Instead of using selectors like CSS classes, the Testing Library provides multiple ways to query elements: e.g. by partial text or by role.
- We can test more easily how accessible a component is. Testing Library helps to write test code that can uncover accessibility issues like missing keyboard navigation.
- The
RenderResult
contains a reference to theComponentFixture
which is under the hood. This means we can still do things like simulating events or manually triggering change detection if we want to. - The test above does very little mocking. If possible I’d like to avoid mocking as much as possible in order to write tests that are as close to reality as possible.
This allowed us to get closer to the state Kent Dodds describes as “The Testing Trophy” (highly recommended blog post!).
As you can see, there’s a stronger focus on integration tests than in the test pyramid mentioned above. In front end projects, a component test can be an integration test (think of smart components that talk to different services and use multiple child components under the hood) but it can also be closer to a unit test (for example a test for button component).
In the post mentioned above, Kent gives a good example which I’d like to repeat in order to highlight the importance of integration tests in front end projects:
It doesn’t matter if your component
<A />
renders component<B />
with propsc
andd
if component<B />
actually breaks if prope
is not supplied. So while having some unit tests to verify these pieces work in isolation isn't a bad thing, it doesn't do you any good if you don't also verify that they work together properly. And you'll find that by testing that they work together properly, you often don't need to bother testing them in isolation.Integration tests strike a great balance on the trade-offs between confidence and speed/expense. This is why it’s advisable to spend most (not all, mind you) of your effort there.
Conclusion
Thanks for reading this post about how to write better tests with Testing Library. While I focused on Angular component tests, you can use Testing Library in all sorts of web projects. With Testing Library you can easily write UI tests that avoid testing implementation details and rather focus on the functionality your UI provides.
Have you tried out Testing Library? What are your experiences with writing UI tests? Let me know in the comments.