Material UI unit tests - hands on
TL;DR - Scenarios -> Setup -> Interact -> Verify
This is the second of two hands-on posts about unit testing the front-end. This time we'll build on the principles from the previous post and write clean UI tests that make sense.
Kicking things off from where we left them, we tested that the Combo Box variation of the AutoComplete component would
- render an input field
- allow focus and typing on the text input
But the component is a lot more advanced than that. A good way to find out the capabilities if you didn't write the component yourself is the documentation. RTFM ring a bell? In order to write efficient unit tests we should break down different behaviours and test them in an orderly manner. If we've verified something in one test, there is no use of verifying the same thing in the next test. I've seen a lot of tests that expect
the same things as previous ones so that is one thing we can stop doing now.
Also if we notice that each test starts the exact same way, maybe we should try to extract the code to avoid duplication as much as possible. Notice that the argument to render are the same six lines of code, so why not extract them into a const in the parent scope, turning this:
Into this:
If we have tests that need some setup to run before each unit test, or before all unit tests, we use the beforeEach
or beforeAll
method, and in the callback argument we specify what needs to be done. If we need to do clean-up there is the corresponding afterEach
and afterAll
methods.
Back to writing actual unit tests, what more scenarios can we think of? In this step, it's advisable to have the component running somewhere and interact with it to get ideas into what is expected by the user, and what might go wrong unless we specifically test it. Let's jot some ideas down:
- show the list of movies on focus
- filter the list of movies according to the input
- ignore case when filtering
- let the user know if there were no matches
Straightforward enough, now let's try to write the tests. You can use debug statements to get a feel for what is rendered on the DOM and how to get a hold of (parts of) the elements you're testing.
For this first test, we use the getAllByRole method to target every DOM element with the option role, since the AutoComplete component gives us a list of options on click. By asserting that the length matches a property of an object that is being rendered, the test doesn't have to be updated if the list is updated. I.e. expect(options.length).toBe(10);
will pass now, but will also break if the list is changed.
Let's continue with the other test cases.
Nothing too crazy, the only thing to note is the last one. When looking at the docs for getByRole
we could see that it throws an error if there are no matches. So expecting the length of options to be 0 is not going to work, since an error will be thrown. Instead, we expect that an error will be thrown. In Jest, we need to do this in a callback function in order for toThrow
to work.
The AutoComplete component has another variant as well: the Multiple Values which displays each selected value as a MUI Chip. Let's try and test that since it's bound to have more corner cases! Again, what is reasonable to check here? How about the following:
- render the input containing a chip with the default value
- add a chip once a value from the list has been selected
- remove a chip if clicked
- remove all chips if clear button is clicked
Easily translatable to it-statements, we get these tests:
The test above will fail! Why? Because the chip is one button, but there is also a clear button in the component. This is visible if you add the debug statement before the expect statement fails. You'll get a printout of the DOM that looks something like this:
The Chip component is a div
with the added role of a button. The div has an attribute which the other button does not called data-tag-index
, lets use that for targeting our MUI Chips. So we need to create a filter for the getAllByRole
method. The filter will take a chip in the form of a HTMLElement
and return whether it has the data-tag-index
attribute. With this filter predicate we can get our chips and nothing else now.
We've been using a utility called userEvent throughout these posts, and as per official docs:
user-event is a companion library for Testing Library that provides more advanced simulation of browser interactions than the built-in fireEvent method.
Note the different usage between e.g. .click()
, .type()
, and .keyboard()
. The keyboard
function let's us fire one-off keyboard events, whereas type
accepts whole strings.
The implementation of remaining test cases cover nothing new and have been omitted from this post. They're available in the repo linked at the bottom of this post. If we run the test suite now using the terminal, we get the following output. Note the structure of the report, and how any english-speaking person can make sense of it.
From here, you should be able to
- come up with different scenarios (the "it-statements"),
- set them up and interact with them,
- verify that the end result is as expected.
The majority of unit tests won't be more complicated than this! You can find the source code for the tests in this repository. Good luck!