Using Angular Testing Library with Test Harnesses
The test cases you write for your own components are mostly the simple ones to write because you know the code base and you're in control of the component's HTML structure. If it's needed you can always change the HTML structure in your template to make it easier to query the element that is needed in a test.
But, what if you want to write tests for a third-party component? Or, for a component that another team wrote? In these cases, you don't have control over the HTML structure and you can't make changes to it. This can make it harder to write tests, and it definitely makes it harder to write maintainable tests.
Even for the components that live in your code base, it can be tricky to query the correct element while keeping your test readable and resilient to changes.
In this post, we go over the combination of Angular Testing Library and Test Harnesses to make writing tests easier and enjoyable.
Angular Testing Library *ByRole
queries
link
In comparison to CSS selectors to find HTML elements, using queries provided by Angular Testing Library makes it easier to find elements.
Specifically, I want to highlight the screen.*ByRole
queries because these provide the most value, and are also the most versatile to use.
Using them within a test case automatically improves the readability of the test, and your test won't break that often compared to the other selectors. It's also a good way to ensure that the appropriate sementic HTML element is used (another plus is that it acts as a solid foundation of accessibility practices).
For example, let's compare the following two test cases:
Both test cases verify that a button is rendered, but the second test is more robust and its intention is also clear.
The second test uses the *ByRole
query to find the button. If the button should be changed in the future, the query continues to find it.
This is because a button is a button and remains a button.
On the other hand, the first test case is brittle. It fails when the template of the component changes, for example, when the class name changes or when the div
is replaced with another element.
But... there are times when there's no semantic HTML element that fits your needs.
Although it's better then nothing, the queries that come with Testing Library to query these elements might still be brittle.
As a workaround, I used to add a data-testid
attribute to be able to query the elements by id.
This is fine for your own components, but you can't do this for components that you don't own.
For these cases, a Test Harness is the perfect solution.
Test Harnesses link
Test Harnesses let you query and interact with the components without having to worry about the DOM structure. This is perfect for testing 3rd party components, and for the base components that are shared in your code base.
The advantage of using a harness is that you can change the DOM structure without breaking the tests, you just have to update the test harness to be "compatible" with the changes made to the template. Instead of going through all the test cases and updating the query, you only need to make one change. With this minimal effort you make sure that your tests are resilient to future changes.
Luckily, we don't need to invent this concept from scratch. The Angular Material CDK provides the infrastructure to write test harnesses for Angular components. Angular Material also has a set of test harnesses for their components. Sadly, not all 3rd party components have test harnesses but if needed you can write your own.
Without going into much detail about test harnesses (you can find an elaborate explanation in the Angular Material CDK documentation with lots of examples), let's look at how to integrate a test harness with the Angular Testing Library.
First, let's create a component that has a button that opens a snack bar when it's clicked.
Before using a test harness, let's take a look at the test for this component without using a test harness.
It looks similar to the test at the beginning of this post, but it uses a screen.*ByText
query to verify that the snack bar is opened.
While this works, the *ByText
query is not very specific, and this could also conflict with other elements on the page when the same text is displayed multiple times.
The below test uses *ByText
because the snack bar isn't wrapped in a semantic HTML element.
We just test that the message is displayed.
Now, let's take a look at the same test, but now with the addition of a test harness.
The component is rendered in the same way, but as an additional step, we load the test harnesses environment.
To load the harness, pass the component's fixture that's returned from the render
method.
Once the test harness environment is loaded, we can use the getHarness
method to query specific elements/components on the page.
In this example, the MatButtonHarness
and MatSnackBarHarness
components.
When we get access to the harness instances, the test uses the methods provided by the harness to interact with the button and to verify that the harness shows the expected message.
While this test uses the test harnesses, I do find it a bit verbose. Because it uses the Child Components within the test it also leaks the implementation details of the component(s).
I could also be opinionated, but I prefer to use the queries provided by Angular Testing Library where it's applicable.
For this case I would replace the button harness with the screen.*ByRole('button')
query, and use user-event
to interact with the button.
For the snack bar, the test harness is a perfect fit because it can't easily be queried with a query from Angular Testing Library.
https://twitter.com/tim_deschryver/status/1603008583645515777
Conclusion link
Ideally, an element should be queryable with Testing Library. When that's not possible, I like to use a Test Harness. Doing this results in a more robust test that is less likely to break when the implementation changes.
But, don't just fall back to a test harness without giving it some thought. Instead, think about how you can make the element queryable. Most of the time this means using semantic HTML.
If it isn't your codebase, you can open an issue on the component's repository. This is a win-win situation because you get a more robust test, and the component gets better, and will likely be better accessible.
In short, using a Test Harness is a good fit to Angular Testing Library when it's not possible to query an element with Testing Library.
Feel free to update this blog post on GitHub, thanks in advance!
Join My Newsletter (WIP)
Join my weekly newsletter to receive my latest blog posts and bits, directly in your inbox.
Support me
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.