Writing tests
In this section, we'll write tests to confirm the behavior of the Todo App.
Let us see where we are currently. Run the dev server and open the respective dev URL, as Vite shows in your browser.
Also, buckle up. This section is going to be a long ride.
Render Test
As frontend developers, we find it easier to work with visible things. We already know that the component is visible in the browser.
But why do we even need to test it? Writing tests for this seems futile. Right now, the component is straightforward. But, as the component grows, it will become more complex and we cannot be sure that any new changes will not cause regressions.
So, writing tests to confirm our current behavior is a good idea. Let us write a test to verify that the input and button are rendering in the browser.
Test Structure
Create a new page.test.ts
file in the src/routes
folder. This file will pretty much contain the tests for the entire Page (or component). Also, this file will be ignored by the SvelteKit router.
Note: We do not name the file +page.test.ts
but just page.test.ts
. This is because files starting with +
are reserved for the SvelteKit router, despite the files being ignored.
Every test should have a meaningful name. The test name should describe what the test is doing. In this case, we are testing if the Page component renders the input field and the add button. (read more about test API here)
Describe is used to group tests. It is not necessary to use describe, but it is good practice for grouping tests. It helps organize the tests and debug them. (read more about describe API here)
Render Component for Testing
We need to render the component in the test. We can use the render
function from @testing-library/svelte
to render the component. (read more about render API here)
Query the DOM
We need to query the DOM to check if the input field and the button are present.
We can use various queries. In general, following the guidelines of Testing Library (read the priority here) for the query priority order is a good idea.
To summarise at a high level, we need to use the queries closest to the user. In our case:
- We'll use
getByRole
to query the input and the button. This comes from thescreen
of@testing-library/svelte
.
Assert
We have the elements. Now, we need to assert that they are present in the DOM. We can use the expect
function from vitest
to assert. (read more about expect API here)
And also, use the toBeDefined
matcher to check if the element is defined. (read more about toBeDefined matcher here)
Run the Test
Now, run the test and see if it passes.
The test should pass. You can delete the sample test index.test.ts
as it is not required.
Improving the Test
Now, toBeDefined
is not the best matcher to use here. We can use toBeInTheDocument
matcher from @testing-library/jest-dom
to check if the element is present in the DOM. (read more about toBeInTheDocument matcher here)
- Read this StackOverflow answer here to understand the difference between
toBeDefined
andtoBeInTheDocument
.
So, let us install @testing-library/jest-dom
and use the toBeInTheDocument
matcher.
We will import the vitest
compatible version of @testing-library/jest-dom
from @testing-library/jest-dom/vitest
. (read more about vitest compatible version here)
And it would import the entire thing.
Rerun the test, and it should pass.
The jest-dom
import (line 3 in the above snippet) usually happens in a separate setup file for all the tests in our application. We will do that now as a good practice.
Create a new file, setup-tests.ts
, inside the src/tests
folder and add the import.
The docs will suggest putting this file at the root of the project but then TypeScript will complain. Then you might try including that file in the tsconfig.json
but then all the SvelteKit includes will not work as they don't get merged from the referenced file.
So, the easiest fix is to put this file in an already included folder which is almost anywhere inside the src
folder.
And remove the import from the test file.
We must also update our vite.config.ts
to use this setup file. (read more about setup files here)
Rerun the test, and it should pass.
Great, we wrote our first test, which made sense to test. But we are not done yet. We need to test the functionality of the component further.
If you also did our React version of this course, you'd notice that we are skipping the eslint
plugins for testing-library
and jest-dom
. This is due to this course using eslint version 9
and the respective plugins not yet compatible.
The option we have is to either go down an eslint
version or wait for the plugins to be compatible. We will go with the latter and update this course the moment they are made compatible.
Testing the user interaction
Let us write a test to check if the user can add a task to the list.
So, how can we test user interaction? If we look at the docs of Testing Library
, you can see a section for User Actions
under the Core API
and a page for Firing Events
. Here's the exact link to the page.
Inputs are usually changed, and buttons are clicked. We pretty much end up with something like this:
Note: We await for the input to be changed before we click the button. This makes sure that the input is actually changed before we click the button. Also, we do not use fireEvent.change
for the input but rather fireEvent.input
, this has to do with Svelte's reactive updates. EITHER WAY, THIS IS NOT A GOOD WAY TO TEST AND WE'LL SOON REFACTOR IT TO USE A BETTER WAY.
Now, if you run your tests, they should be passing! But there is still a problem:
- To amplify the problem significantly, let us make the input field
readonly
and see if our tests still pass.
Our tests will still pass! But, we know that the user cannot type in the input field. What we have right now is a false positive.
We could have avoided this problem by testing user interaction differently. Even the docs recommend another library called user-event
for testing user interaction. Here's the link to the docs
Let us install user-event
and use it in our tests.
Since user interactions are asynchronous (just like how we did for fireEvent), we need to use async
, await
, and waitFor
from @testing-library/svelte
for the changes to take effect. (read more about async methods)
Our tests should fail. Because we are using readonly
on the input field, remove the readonly
attribute from the input field and rerun the tests.
Our tests should pass now. We also have a more robust test for user interaction.
Now, we have made a decent amount of progress on tests. Remember where we left our actual business logic? In the next section, let us return to that with TDD
(Test Driven Development).
At this point, your code should be a good match to the branch of the repository: 5-writing-tests