Component Tests, TypeScript and the Next Iteration of Vue Test Utils
Learn some best practices for writing component test, and get a sneak-peak at the new version of Vue Test Utils, built in TypeScript for Vue 3.
To do this, I will be building a simple Todo app, and writing tests for the features as we go.
This article is available as a screencast on my website, as well as many more! You can watch it right here as well:
Getting Started
We get started with a minimal App.vue
:
The <template>
is currently empty - we will write a failing test before we work on that. Other than that, everything is pretty standard - since we are using TypeScript, we are able to type the ref
using a Todo
interface, which makes the content of a Todo much more clear to the reader.
A best practice: data-test
selectors
Let’s write the first test. We want to verify the todos
are rendered.
I am searching for the todo
item is a data-test
selector. I have found these really useful - things like classes and ids are prone to changing over time. By adopting a data-test
convention, it's clear to other developers those tags are used for tests, and they should not be changed or removed.
Of course this test is failing — let’s get it to pass.
Completing a Todo
The next feature I’ll be implementing is the ability to complete a todo. Let’s write a test first.
Again, we are using the data-test
selector. We also see a new feature to the latest version of Vue Test Utils - we are now able to await
any method that might cause the DOM to rerender, such as setChecked
. We need to do this because Vue renders asynchronously, and if we do not await
, it is possible our assertion is called before the DOM has updated.
I am asserting that the class
contains completed
- using some CSS, I can show which todos are completed by using the completed
class and some styling, such as text-decoration: strike-through;
.
Let’s get this test to pass. We just need to update <template>
:
Adding a new Todo
The last feature we will be adding, and subsequently refactoring, is a form that lets a user add a new todo. As usual, let’s write a test that will help us think about how we want to implement the feature.
Since this test is mostly concerned with the number of todos (increasing from 1 to 2) we are focusing our assertions on the number of todos rendered. At this point, my test is not only failing, it’s not even compiling — ts-jest
is reporting an error:
At first this is confusing. I intend on using an <input>
element for the user to type the new todo - and of course an <input>
element has a value
property - so why is this error appearing?
The reason is that even though we know that data-test="new-todo"
will refer to an <input>
element, TypeScript does not. For this reason, find
is generic in the newest version of Vue Test Utils - the signature looks like this: find<T>(selector: string) => T
. We can hint at what find
should return. Updating that line looks like this:
Now we can get suggestions for the properties on element
from the IDE. Great!
Now our test is compiling (and failing), let’s actually implement the feature. Only the new code is shown for brevity:
Nothing especially unusual — we can see TypeScript and the Todo
interface assisting us, ensuring we do not miss any properties in the new todo. With this code, everything is passing.
Refactoring the new todo component
We are going to do a refactor, that will reveal some interesting facts about our tests. Let’s imagine we now need to persist new todos to a server, so we want to make an API call when we submit the form. Since the form is getting complex, and may continue to do so, we decide to move to it’s own component, TodoForm.vue
. Let's move the logic from App.vue
to TodoForm.vue
:
The only real change is instead of todos.value.push
to add the new todo to the array, we are using ctx.emit
to emit a createTodo
event with the new todo at the first parameter. We set -1
to the id
temporarily, since we do not know the length of the todos
array in this component.
The test is now failing — let’s import the new TodoForm.vue
component, and see what happens. Again, only the changed code is shown:
We basically just removed the <form>
and replaced it with <TodoForm />
- and all the tests are passing again. This is a very good thing - since the behavior did not change, the tests should not need to change either. If a refactor breaks your tests, it (usually) means you are testing implementation details, not behavior. The user doesn't care about how things work, they care that they work correctly, so that's what your tests should reflect.
Even though we don’t have a server to run the code, we could go ahead and implement the posting of a new todo to a server. Let’s do that — TodoForm.vue
setup function now looks like this:
The test is now failing with all sorts of errors. Let’s mock out axios with jest.mock
at the top of our test:
The test is green again — great! We could even update the test to verify that Do work
is now rendered as the second todo, if we wanted.
Discussion and Conclusion
This article covered a few best practices, namely:
- using
data-test
selectors in tests - testing behaviors, not implementations
One thing you may have noticed is we have no tests for TodoForm.vue
- this is intentional. We test is implicitly via the tests for App.vue
. If TodoForm.vue
grew in complexity, I would consider writing specific tests for some of its more complex behavior - but I would still keep the tests we just wrote, since those cover the integration between the two components. This gives me confidence my system is working correctly.
Original published on Vue.js Courses.