DEV Community

Cover image for How to write tests in Sveltekit and Vitest
Eternal Dev for Eternal Dev

Posted on • Originally published at eternaldev.com on

How to write tests in Sveltekit and Vitest

đź’ˇ As of the writing of this post, both SvelteKit and Vitest are not ready for production usage. SvelteKit is very promising but not recommended for production use until they are out of the beta stage

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
Enter fullscreen mode Exit fullscreen mode

ProjectInit.png

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.

OutputScreenshot.png

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/

  1. Install vitest as a dev dependency

  2. Add a config file for vitest (You can use existing vite.config.js if you have that file)

  3. Adding the vitest test script in package.json

scripts: {
        ...
        "test": "vitest",
    "coverage": "vitest run --coverage"
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Describe block - This is used to group related tests and create a block of content
  2. it block - Used to create and provide a describing test which is easily recognizable
  3. 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);
    })
})
Enter fullscreen mode Exit fullscreen mode

After adding this file, you can run the test watch command

npm run test -- --watch
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
    })
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Red stage - Write a test and watch it fail (Red)
  2. Green stage - Write the most basic logic needed to make the test pass (Green)
  3. Refractor stage - Refractor the code to make it better than before

TddIntro.jpg

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();
    })
})
Enter fullscreen mode Exit fullscreen mode

FirstTestFail.png

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}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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: ''}});
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

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));
    })

})
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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));
  })
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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.

AllTestPassed.png

Coverage report

Adding coverage report is easy by installing a package c8

npm install --save-dev c8
Enter fullscreen mode Exit fullscreen mode

Add the following line to the package.json

"scripts": {
    ...
    "coverage": "vitest run --coverage"
}
Enter fullscreen mode Exit fullscreen mode

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.

coverage_report.png

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.

Top comments (0)