How to write tests in Sveltekit and Vitest
We will learn about setting up Sveltekit and Vitest for writing tests and we will build an async component using test-driven development (TDD) methodology in this post.- Sriram Thiagarajan
- March 16, 2022
How to write tests in Sveltekit and Vitest
Introduction
We will make use of Test Driven Development (TDD) in this article to test and develop an async component in Sveltekit
Test Driven Development has always been the major thing that we want to follow on every new project which we start. It means that we write the test cases before the actual implementation of the functionality and make sure all the things are tested and maintain a very high standard of quality. Hopefully, with Vitest and Sveltekit we can start every project with TDD. Vitest is a unit-testing framework powered by Vite.
Setting up the Sveltekit project
Let’s create a new Sveltekit project by following the official documentation - https://kit.svelte.dev/
npm init svelte@next my-app
cd my-app
npm install
npm run dev -- --open
For more about Sveltekit, check out the article on the official blog - https://svelte.dev/blog/whats-the-deal-with-sveltekit
What are we building?
We are going to build a component that will fetch data from a Pokemon API and display the pokemon data. This component will be an async component that will have the loading, success, and error states. This will be a very simple example to explain how we can perform TDD with Vitest and Sveltekit.
What is Vitest?
Vitest is one of the latest packages which hopes to take advantage of Vite’s fast pace and build a testing framework that is fast and easy to use. It is an open-source package that is still in the early stage of development (0.6.2) as of the writing of this post. One of the main advantages of the vitest is that it will work with the default configuration of vite which is used for config of your app. So you don’t need a separate config for your testing. It has a similar syntax as jest so that migration and learning are easy.
There are more significant features which are listed on the official website
https://vitest.dev/guide/features.html
Configure Vitest in SvelteKit
Official Guide - https://vitest.dev/guide/
-
Install vitest as a dev dependency
-
Add a config file for vitest (You can use existing vite.config.js if you have that file)
import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' export default defineConfig({ plugins: [ svelte({ hot: !process.env.VITEST }), ], test: { globals: true, environment: 'jsdom', }, })
Example for svelte from official docs - https://github.com/vitest-dev/vitest/blob/main/examples/svelte/vitest.config.ts
-
Adding the vitest test script in package.json
scripts: {
...
"test": "vitest",
"coverage": "vitest run --coverage"
}
Create a test file in vitest
Vitest API is very similar to Jest. There is also a guide from the official page on how to migrate from jest
https://vitest.dev/guide/migration.html#migrating-from-jest
- Describe block - This is used to group related tests and create a block of content
- it block - Used to create and provide a describing test which is easily recognizable
- expect statements - Used to validate the test results. These form the basis of our test since the test passing/ failing is dependent on these
expect
statements.
Official API Docs - https://vitest.dev/api/
Create a new file - sample.spec.ts
import {describe, expect, it} from 'vitest';
describe("Sample Test Block", () => {
it("sample test which should be true", () => {
expect(true).toBe(true);
})
})
After adding this file, you can run the test watch command
npm run test -- --watch
Creating a svelte component in Sveltekit
Create a new file called PokemonDetails.svelte
inside the component folder in the src directory. We can just write a simple h2
message in that component. Our main objective is to see if we can load this component inside the test and verify that the component is loaded.
PokemonDetails.svelte
<h2>Pokemon</h2>
Mounting the svelte component in the test
Create a new file called PokemonDetails.spec.ts
in the same folder as the PokemonDetails
component and we can try to import that component in the test.
describe("Pokemon Details", () => {
let instance = null;
beforeEach(() => {
const host = document.createElement('div');
document.body.append(host);
instance = new PokemonDetails({ target: host});
})
it('Should show a loading spinner when making the API Call', () => {
expect(instance).toBeTruthy();
})
})
This is a very naive way of adding the component to the document and testing if the component is mounted. This test will return true, but it is difficult for us to test further with this setup. It is difficult to test what things are rendered inside the component using this method. So we are going to take the help of another library svelte-testing-library
to make things easier for us.
Adding the Svelte Testing library in Sveltekit
It makes it easy to render the components and get the details about the different elements inside the component
Official API - https://testing-library.com/docs/svelte-testing-library/api
You can install this in the package.json using the following command
npm install --save-dev @testing-library/svelte
A quick introduction to Test Driven Development (TDD)
Test Driven Development is the process of creating a failing test first, then implementing the logic to make the test pass. This will ensure that the functionality is tested properly with automation tests and it can ensure a higher quality of the product. TDD is often difficult to practice as it is more time-consuming than just writing the logic first. Time spent initially in the setup and writing comprehensive test will result in more time saved in debugging bugs when the application is getting bigger and bigger.
For a more detailed explanation on TDD - https://www.freecodecamp.org/news/an-introduction-to-test-driven-development-c4de6dce5c/
One more important cycle in TDD is the cycle of three-stage
- Red stage - Write a test and watch it fail (Red)
- Green stage - Write the most basic logic needed to make the test pass (Green)
- Refractor stage - Refractor the code to make it better than before
Add your first test for the async component (Red stage)
We are going to create a test that will test if the component is showing a loading message. when the test is running, we should see that the test is failing as expected.
We are going to make use of @testing-library/svelte
to render the Component. In using the render
function, we get access to a bunch of helper functions which will let us test the content inside the component. Here we are going to use getByText
which will return the element if the text is found, otherwise throws an exception.
So we are using that to see if the “Loading…” text is present in the component.
import {beforeEach, describe, expect, it} from 'vitest';
import { render } from '@testing-library/svelte';
import PokemonDetails from './PokemonDetails.svelte';
describe("Pokemon Details", () => {
it('Should show a loading spinner when making the API Call', () => {
const {getByText} = render(PokemonDetails);
expect(() => getByText(/Loading.../i)).not.toThrow();
})
})
Adding your code to fix our test(Green Stage)
We are calling the Pokemon API to get the details of the pokemon and displaying the details on the component.
<script>
let data = null
const getPokemon = async () => {
var response = await fetch('https://pokeapi.co/api/v2/pokemon/1/');
var result = await response.json();
data = result;
}
getPokemon();
</script>
{#if !data}
<h2>Loading...</h2>
{:else}
<h2>Pokemon </h2>
{/if}
After adding the above code, the unit test should pass since the initial message on the component will be “Loading…”
Refractor the code (Refractor stage)
At this point in time, we know that our test is working. So we can refactor our code to make it better. This is essential as we are going to improve the quality of our code even though our tests is passing after the previous step. We are going to make use of the Svelte async/ await syntax to show the loading message which is simpler than our previous method.
For more detailed explanation on how to make an API call, you can look at this article - https://www.eternaldev.com/blog/how-to-make-an-api-call-in-svelte/
<script>
const getPokemon = async () => {
var response = await fetch('https://pokeapi.co/api/v2/pokemon/1/');
var result = await response.json();
return result;
}
let pokemonPromise = getPokemon();
</script>
{#await pokemonPromise}
<h2>Loading....</h2>
{:then pokemon}
<h2>Pokemon </h2>
{/await}
Async component testing in vitest
We have the data now coming from the API, we can add some simple HTML elements to display that data once the data is received. Let’s write the test first to test the functionality. Since we are using the async component, we need to add the async test in vitest.
Adding async it block in vitest
We can use the async keyword before the method to create an async test. Inside the method, we can use the await
keyword and the execution will continue only when the awaiting promise is resolved/rejected.
it('should show the data',async () => {
await someMethod();
}
Mocking the global fetch in vitest
We are going to use the global fetch to call the API in the component. So we need a way to mock this method when running the test. Mocking is a really important part of the writing test since we don’t want the test to be flaky or dependent on the network connection and so on. We want the test to detect if the component is working correctly and not test the network part. So we need to mock the API and return a response that we can control.
We can do this using the mockImplementation
function in Vitest.
global.fetch = vi.fn().mockImplementation(() => {
return Promise.resolve({
json() {
return Promise.resolve({name: 'Test Poke', height: 3, weight: 20, sprites: {front_default: ''}});
}
});
});
Waiting for the text to show up
After that we need to make sure, we are testing the UI only after the promise is resolved. So we are using the waitFor
method which will wait for some time before making the assertion. There is a default timeout for this function and if the timeout is exceeded and still the element is not present, it will throw an exception and fail the test.
it('should show the data',async () => {
const {getByText} = render(PokemonDetails);
await waitFor(() => getByText(/Pokemon: Test Poke/i));
});
After doing all the three above steps, we are now able to test for the async component in Sveltekit and Vitest. Below is the complete code for that test
import {beforeEach, describe, expect, it, vi} from 'vitest';
import { render, waitFor } from '@testing-library/svelte';
import PokemonDetails from './PokemonDetails.svelte';
describe("Pokemon Details", () => {
beforeEach(() => {
global.fetch = vi.fn().mockImplementation(() => {
return Promise.resolve({
json() {
return Promise.resolve({name: 'Test Poke', height: 3, weight: 20, sprites: {front_default: ''}});
}
});
});
});
it('Should show a loading message when making the API Call', () => {
const {getByText} = render(PokemonDetails);
expect(() => getByText(/Loading.../i)).not.toThrow();
})
it('should show the data',async () => {
const {getByText} = render(PokemonDetails);
await waitFor(() => getByText(/Pokemon: Test Poke/i));
await waitFor(() => getByText(/Height: 3/i));
await waitFor(() => getByText(/Weight: 20/i));
})
})
After the test fails, we can update the PokemonDetails.svelte
component to make the test pass.
<script>
const getPokemon = async () => {
var response = await fetch('https://pokeapi.co/api/v2/pokemon/1/');
var result = await response.json();
return result;
}
let pokemonPromise = getPokemon();
</script>
{#await pokemonPromise}
<h2>Loading....</h2>
{:then pokemon}
<h2>Pokemon: {pokemon.name}</h2>
<h3>Height: {pokemon.height}</h3>
<h3>Weight: {pokemon.weight}</h3>
{/await}
Adding the error handling of the API
Finally, we need to add an error handling part to the component when there is an error from the API or the network connection is not working.
Since we want the mock implementation to reject the promise for this test, we can override the mock only for this test.
it('should show error when the API fails', async () => {
global.fetch = vi.fn().mockImplementationOnce(() => {
return Promise.reject();
});
const {getByText } = render(PokemonDetails);
await waitFor(() => getByText(/Error while loading the data/i));
})
We can update the component to add that error text after the test is failing.
<script>
const getPokemon = async () => {
var response = await fetch('https://pokeapi.co/api/v2/pokemon/1/');
var result = await response.json();
return result;
}
let pokemonPromise = getPokemon();
</script>
{#await pokemonPromise}
<h2>Loading....</h2>
{:then pokemon}
<h2>Pokemon: {pokemon.name}</h2>
<h3>Height: {pokemon.height}</h3>
<h3>Weight: {pokemon.weight}</h3>
{:catch err}
<h2>Error while loading the data</h2>
{/await}
Yay! we now have all the tests to test the whole async component and all of them are passing now. This is a good start for your application and you can continue adding more complicated tests which is suitable for your application.
Coverage report
Adding coverage report is easy by installing a package c8
npm install --save-dev c8
Add the following line to the package.json
"scripts": {
...
"coverage": "vitest run --coverage"
}
So when you run the npm run coverage
you will get the coverage report in the terminal. It will also create a new folder coverage
in your source directory which will contain all the detailed information about the coverage.
Conclusion
We have actually been really impressed by the vitest package and it seems to have very seamless integration and working with the familiar jest like API has been a delight so far. It also seems to be really fast but those metrics can be calculated with a more complicated real-world project than this sample project. It seems to have a huge potential since it is not Svelte specific and it can be used with other frameworks as well. We are really looking forward to using more features of this package.