Testing a Svelte app with Jest
Tags: webdev, javascript, svelte, testing
Published: 2021-11-18 10:18:57

---
description: "In this tutorial, I will go through using Svelte with Vite, and show you how to test your components with Jest. I will be using JavaScript, but I will mention the extra steps you need to take if you want to use TypeScript instead. I will also test a Todo app to show you what testing looks like on an actual app."
---

I have seen very little written about testing Svelte components. I have yet to see a tutorial build *and* test a Svelte app! This is disconcerting. Maybe, testing is not considered a sexy topic, or for hobby projects people like to walk on the wild side. I don't know. In any case, it is not a good idea for any project to skip it! "Practice as you intend to play" is my philosophy!

[Svelte](https://svelte.dev/) hasn't anointed a set of testing tools or does not advocate for a particular testing strategy. It gives some [basic advice](https://svelte.dev/faq#how-do-i-test-svelte-apps). More established frameworks have recommendations and integrations specific to their CLI tooling - [React recommends using Jest and React Testing Library](https://reactjs.org/docs/testing.html), and [Vue recommends using Mocha or Jest with Vue Testing Library](https://vuejs.org/v2/guide/testing.html). In theory, you can use whatever JavaScript testing library you want for testing, because in the end you will be testing JavaScript code, regardless of whether it is transpiled or not. However, it can prove to be tricky to integrate different tools into a frontend toolchain for your "dev stack".

Svelte has relied on [Rollup](https://rollupjs.org/guide/en/) for as the central point for it's dev tooling so far, but recently [Vite](https://vitejs.dev/) has been adopted by [SvelteKit](https://kit.svelte.dev/). Vite is among the next generation frontend tooling brigade. It provides a much faster dev environment, hence the name, vite means fast in French. It uses native ECMAScript Modules (ESM) to provide on-demand file serving, which means updates are instantly reflected without reloading the page or blowing away application state.

While the new direction for Svelte appears to be set, the current state of affairs is that most testing frameworks are still "last generation"! They mostly use [commonJS modules](https://en.wikipedia.org/wiki/CommonJS) and need to adjust to this new paradigm. You can see the issue ["feature: first class Jest integration"](https://github.com/vitejs/vite/issues/1955) in the Vite GithHub repo to see some of the issues you can run into. In the meantime, you need to transpile your code and do some extra hacks and configuration to get everything to play nice. This is never fun!

In this tutorial, I will go through using Svelte with Vite, and show you how to test a complete app with Jest. I will be using JavaScript, but I will mention the extra steps you need to take if you want to use TypeScript instead. I will be testing a simple Todo app to  clearly demonstrate what testing looks like without too much complexity or clutter.

Let's get to it!

## TLDR

Here are the GithHub repositories for the code I cover in the article:

- Starter template - [https://github.com/robole/svelte-vite-jest-template](https://github.com/robole/svelte-vite-jest-template).
- Todo app - [https://github.com/robole/svelte-todo-with-tests](https://github.com/robole/svelte-todo-with-tests).

### Getting started from a template

Let's create a Svelte project based on the Vite "svelte" template, and call it *example-svelte-app*. For TypeScript, use the "svelte-ts" template instead.

With NPM 7+, you must supply an extra set of double hypens :

```bash
npm init vite@latest example-svelte-app -- --template svelte
cd example-svelte-app
npm install
```

With Yarn:

```bash
yarn create vite example-svelte-app --template svelte
cd example-svelte-app
yarn install
```

With PNPM:

```bash
pnpm create vite example-svelte-app --template svelte
cd example-svelte-app
pnpm install
```

Now, we have a default project. It says "HELLO WORLD!" and has a `Counter` component. We can run the project with `npm run dev` and visit it at [localhost:3000](localhost:3000).

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wovvn6vfz6t6s8l4xwde.png" alt="simple cover image of vs code logo" width="959" height="547" loading="lazy"/>

### Configuration

We need the following libraries to get set-up for testing:

1. *[Jest](https://www.npmjs.com/package/jest)* is the test runner that we will use. It also has some assertion and mocking functionality.
2. *[@babel/core](https://www.npmjs.com/package/@babel/core)*, [*babel-jest*](https://www.npmjs.com/package/babel-jest)  and [*@babel/preset-env*](https://www.npmjs.com/package/@babel/preset-env) are required for the transpilation Jest requires. Jest uses commonJS by default, and we are using ECMAScript Modules (ESM) in our code, so we need to get them in the same form. The latest version of Jest  is [v27.2](https://jestjs.io/docs/ecmascript-modules) and has *experimental* support for ESM. I did not want to go down the experimental road! Hopefully, this will mature quickly and remove the need for Babel in the toolchain if you are using JavaScript.
3. [*svelte-jester*](https://www.npmjs.com/package/svelte-jester) and [*jest-transform-stub*](https://www.npmjs.com/package/jest-transform-stub). Jest does not understand how to parse non-JavaScript files. We need to use  `svelte-jester` to transform Svelte files, and `jest-transform-stub` for importing non-JavaScript assets (images, CSS, etc).
4. *[@testing-library/svelte](https://www.npmjs.com/package/@testing-library/svelte)* (known as Svelte Testing Library) provides DOM query functions on top of Svelte in a way that encourages better testing practices. Some of the most commonly used functions are `render`, `getByText`, `getByLabelText`, and `getByRole`.
5. *[@testing-library/user-event](https://www.npmjs.com/package/@testing-library/user-event)* is a companion library to Svelte Testing Library that provides more advanced simulation of browser interactions than the built-in [`fireEvent`](https://testing-library.com/docs/dom-testing-library/api-events#fireevent) function. An example of this is if you need to trigger an event for a mouse click while the `Ctrl`  key is being pressed. <u>You may not need this, but it is worth knowing about it.</u>
6. If you use [global environment variables](https://vitejs.dev/guide/env-and-mode.html) or a `.env` file in your code, you need to install [babel-plugin-transform-vite-meta-env](https://www.npmjs.com/package/babel-plugin-transform-vite-meta-env) to transform these variables for the commonJS module. This is <u>not a permanent solution</u> (famous last words, I know). You can [read this issue](https://github.com/vitejs/vite/issues/1149) for more details on the hopes for better integration where this would not be necessary.
7. [*@testing-library/jest-dom*](https://www.npmjs.com/package/@testing-library/jest-dom) provides a set of custom jest matchers that you can use to extend jest. These can be used to make your tests more declarative. It has functions such as `toBeDisabled()`, `toBeInTheDocument()`, and `toBeVisible()`. <u>This is optional too</u>.
8. <u>If you are using Typescript</u>, you need to install [*svelte-preprocess*](https://github.com/sveltejs/svelte-preprocess) and [*ts-jest*.](https://github.com/kulshekhar/ts-jest) also.

We need to install these libraries and do some configuration before we can get to our tests:

1. I will install the aforementioned libraries with NPM without the TypeScript dependencies:

    ```bash
    npm install -D jest babel-jest @babel/preset-env svelte-jester jest-transform-stub @testing-library/svelte @testing-library/user-event babel-plugin-transform-vite-meta-env @testing-library/jest-dom
    ```

2. We need to configure Jest to transform our files. We must explicitly set our test environment to *jsdom*, which we are using through Jest. Since v27 Jest's default test environment is *node*. I will put the configuration in a specific Jest configuration file called *jest.config.json* in the project root folder. If you create a configuration file called *jest.config.js*, Vite will complain as it expects only ESM JavaScript by default. Vite will recommend that you rename it to a ".cjs" file if you want to do it that way. You can look at the [different ways to configure Jest](https://archive.jestjs.io/docs/en/next/configuration) if you are unsure about the file conventions. If you're using TypeScript, you need to configure [*svelte-preprocess*](https://github.com/sveltejs/svelte-preprocess) and [*ts-jest*](https://github.com/kulshekhar/ts-jest) also, see the [*svelte-jester*](https://github.com/mihar-22/svelte-jester#typescript) docs for how to do that.

    ```json
    {
      "transform": {
        "^.+\\.js$": "babel-jest",
        "^.+\\.svelte$": "svelte-jester",
        ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
      },
      "moduleFileExtensions": ["svelte", "js"],
      "testEnvironment": "jsdom",
      "setupFilesAfterEnv": ["@testing-library/jest-dom/extend-expect"]
    }
    ```

3. We configure Babel to use the current version of node. Include the [babel-plugin-transform-vite-meta-env](https://www.npmjs.com/package/babel-plugin-transform-vite-meta-env) plugin if you are using environment variables. I will put the configuration in a *.babelrc* file in the project root folder. If you are using TypeScript, you need to add a TypeScript preset also, see the [Jest docs](https://jestjs.io/docs/getting-started#using-typescript) for the details.

    ```javascript
    {
      "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]],
      "plugins": ["babel-plugin-transform-vite-meta-env"]
    }
    ```

4. Add the scripts to run the tests in your `package.json`

   ```json
   "test": "jest src",
   "test:watch": "npm run test -- --watch"
   ```

5. Let's see if our set-up is correct by running `npm run test`. Since we don't have any tests yet, you should see following message in console.

	```
	➜ npm run test> example-svelte-app@0.0.0 test
	> jest src
	
	No tests found, exiting with code 1
	```

Whew, that's a lot! I wasn't lying when I said that it can prove to be tricky to integrate different tools into a frontend toolchain! 😅

If you are using SvelteKit, this should work also. I have not delved into SvelteKit yet, so I don't know if something slightly different is required. If there is, let me know!

### Your first unit test

Now, lets create a test module for our *App.svelte* component called  *App.spec.js* in the same folder. By default Jest looks for filenames that end with either ".spec.js" or ".test.js".

```javascript
import { render, screen } from '@testing-library/svelte';
import App from './App.svelte';

test("says 'hello world!'", () => {
    render(App);
    const node = screen.queryByText("Hello world!");
    expect(node).not.toBeNull();
})
```

We need to import the component, and the functions we use from the Svelte Testing Library.

We pass our component to the `render` function to setup our component. Svelte Testing Library creates a `screen` object for us that is bound to `document.body` of the virtual document. We can use this to run some of the builtin DOM query functions against.

Here, we use the `queryByText` function to look for an element with that text content. It will return a node object if it finds an element with that text. It will return `null` if no elements match.

> For details on the query functions , see the [DOM Testing Library’s “Queries” documentation](https://testing-library.com/docs/dom-testing-library/api-queries). Some of the most commonly used query functions are `getByText` and `getByLabelText`.

Next, we use some of Jest's [expect matchers](https://jestjs.io/docs/expect) to check that the node is not null.

Alternatively, you can use `expect(node).toBeInDocument()` from *@testing-library/jest-dom*. This is a bit easier to read I guess(?), so we will use this from now on.

When we run the test, we get the folllowing output:

```
 ➜ npm run test 
 
> example-svelte-app@0.0.0 test> jest src

PASS  src/App.spec.js  
   ✓ says 'hello world!' (33 ms)

Test Suites: 1 passed, 1 totalTests:       
1 passed, 1 totalSnapshots:   0 total
Time:        1.711 s
Ran all test suites matching /src/i.
```

You don't need to destroy the component after each test, this is [done automagically for you](https://testing-library.com/docs/svelte-testing-library/api#cleanup)!

Typically, you would explicitly create a test suite for each component with the function `describe(name, fn)`. We wrap our tests in a function and pass it as the second argument. It usually look like this:

```javascript
describe("App", () => {
  test("says 'hello world!'", () => {
    render(App);
    const node = screen.queryByText("Hello world!");
    expect(node).toBeInTheDocument();
  });
});
```

You will see that some people use the `it()` function instead of `test()` also. It's the [same thing](https://stackoverflow.com/questions/45778192/what-is-the-difference-between-it-and-test-in-jest), just a different style. The `it` function is influenced by [rspec](https://en.wikipedia.org/wiki/RSpec).

### Testing events

Lets test our `Counter` component by creating a *Counter.spec.js* file in the same folder (lib).

```html
<script>
  let count = 0

  const increment = () => {
    count += 1
  }
</script>

<button on:click={increment}>
  Clicks: {count}
</button>
```

Whenever the button is pressed, it increments a `count` variable that is displayed in the button label.

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4afw9osfnxqzsgds12jw.png" alt="counter component" width="323" height="106" loading="lazy"/>

We will create a similar test to our first test for the `App`. We just want to check that the button is rendered.

```javascript
import { render, screen, fireEvent } from "@testing-library/svelte";

import Counter from "./Counter.svelte";

describe("Counter", () => {
  test("it has a button with the text 'Clicks: 0'", async () => {
    render(Counter);

    const button = screen.getByText("Clicks: 0");
    expect(button).toBeInTheDocument();
  });
});
```

Now, we want to check the action will increment the count. This is where we reach for the [`fireEvent`](https://testing-library.com/docs/dom-testing-library/api-events#fireevent) function. There is a convenient form of the function `fireEvent[eventName](node: HTMLElement, eventProperties: Object)` where we can provide the event name as a suffix. So,  we can write `fireEvent.click(screen.getByText("Clicks: 0")`. Because this is an asynchronous event, we need to use the `await` syntax and make our test function `async`. The test function looks this:

```javascript
  test("it should increment the count by 1 when it the button is pressed", async () => {
    render(Counter);

    const button = screen.getByText("Clicks: 0");
    await fireEvent.click(button);

    expect(screen.getByText("Clicks: 1")).toBeInTheDocument();
  });
```

You can use the [user-event library](https://testing-library.com/docs/ecosystem-user-event/) instead, but **be aware that all events are treated as async in Svelte testing**. For other frameworks, they are probably synchronous. This is unique to the Svelte because the library must wait for the next `tick` so that Svelte flushes all pending state changes.

We can check the test coverage of our app now by running `npx jest --coverage`.

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ugsbmx3jo1rlnz2x7e75.png" alt="counter component" width="891" height="505" loading="lazy"/>

And we're at 100% coverage. Yay!

## Unit tests for a Todo app

While we're at it, let's test a more complete app. This is where we can really see what testing is like. Let's look at a minimal Todo app.

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xh0yntjw8z5mk2ajsxbn.png" alt="todo app screenshot" width="556" height="357" loading="lazy"/>

### Requirements

The app should do the following:

1. List todos. When there are no items, the message "Congratulations, all done!" should be shown.
2. Allow a user to mark/unmark todos as done. When a todo is done, it is styled differently. The text color is gray and has a strike-through decoration.
3. Allow a user to add new todos, but prohibit the addition of an empty todo.

We will write our tests on these requirements.

### Component overview

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ok5gf3852i3ucq16an31.png" alt="todo components figure" width="768" height="106" loading="lazy"/>

1. The `App` component contains the other components. It has a subheading that shows the status of the todos e.g "1 of 3 remaining ". It passes an array of todos to `TodoList`. We hardcode 3 todos in our app , as per screenshot above.
2. The `AddTodo` component contains the form with an text input and button to add new todos to our list.
3. The `TodoList` component is an unordered list of the todos. It has a `todos` prop that is an array of todo objects. Each list item contains a `Todo` component.
4. The `Todo` component shows the text of the todo and has a checkbox for marking the item as done. It has a `todo` prop that is a todo object.

The child components dispatch events up to the `App` when there are data changes from user interaction. For example, `Todo` dispatches a `toggleTodo` event whenever it's checkbox is clicked, this event is forwarded by `TodoList` to `App` to handle this event.

### Tests

I will highlight a couple of the unique aspects of the tests to demonstrate some of the methods for using Jest.

#### Testing with props and classes (`Todo.spec.js`)

This is an example of passing props to components when we are testing. We pass them through an object we provide as the second argument to the `render` function.

```javascript
describe("Todo", () => {
  const todoDone = { id: 1, text: "buy milk", done: true };
  const todoNotDone = { id: 2, text: "do laundry", done: false };

  test("shows the todo text when rendered", () => {
    render(Todo, { props: { todo: todoDone } });

    expect(screen.getByLabelText("Done")).toBeInTheDocument(); //checkbox
    expect(screen.getByText(todoDone.text)).toBeInTheDocument();
  });
    
  //etc..
});
```

In this test case, we want to get the checkbox for the todo. It has a lable of "Done", so we can get it through the function `getByLabelText()`. The checkbox has an `aria-label` attribute rather than a corresponding `label` element, it does not matter which it is. I like to favour using this function as it is a a good reminder to ensure that every input should have a label to keep things accessible for everyone.

Next, we want to test when a Todo item is marked as done.

```javascript
test("a done class should be added to the text item when a todo is done", () => {
    render(Todo, { props: { todo: todoDone } });

    expect(screen.getByText(todoDone.text)).toHaveClass("done");
});
```

When the checkbox is checked, a `done` class is added to the `span` element that has the todo text. We can use the `toHaveClass()` function to check that this class is added correctly for done todos.

### Testing text entry (`AddTodo.spec.js`)

To simulate a user entering text into the textbox, we use the `type` function from the *@testing-library/user-event* library. In this case, the button is only enabled when text is entered.

```javascript
import { render, screen } from "@testing-library/svelte";
import userEvent from "@testing-library/user-event";

import AddTodo from "./AddTodo.svelte";

describe("AddTodo", () => {
  // other stuff
 
   test("the add button should be enabled when text is entered", async () => {
    render(AddTodo);

    await userEvent.type(screen.getByLabelText("Todo"), "abc");
    expect(screen.getByRole("button")).toBeEnabled();
  });
});   
```

#### Testing data mutation (`App.spec.js`)

You may have expected the adding of a new todo to be tested in `AddTo.spec.js`. However, since the `AddTodo` component doesn't result in a DOM change, rather it fires an `AddNew` event, there is no way for us to test it through DOM query methods. The action is delegated to the `App` component, so this is where we will test it.

```javascript
import { render, screen, fireEvent } from "@testing-library/svelte";

import App from "./App.svelte";

describe("App", () => {
  const PREDEFINED_TODOS = 3;
    
  // other stuff
    
  test("should add a todo", async () => {
    render(App);

    const input = screen.getByLabelText("Todo");
    const value = "Buy milk";
    await fireEvent.input(input, { target: { value } });
    await fireEvent.click(screen.getByText("Add"));

    const todoListItems = screen.getAllByRole("listitem");

    expect(screen.getByText(value)).toBeInTheDocument();
    expect(todoListItems.length).toEqual(PREDEFINED_TODOS + 1);
  });
    
});
```

In this test case, we must simulate inserting some text to the textbox, and then hitting the "Add" button. I use `fireEvent.input` to pass the text to the textbox to its `value` property. This function is similar to `userEvent.type` that I used in the previous example. I use it here to show you both ways, use whichever you prefer. Don't forget that these actions are asynchronous, so always use `await`.

For our test assertion, we want to check that the text for our new todo is now added to the document. This should be familiar by now - `expect(screen.getByText(value)).toBeInTheDocument();`.

We can be doubly sure of the success of our action by checking the number of todos in the page. Because the todo items are added to the only list in the page, we can check the number of todos by getting elements that match the accessibility role of `listitem` through `screen.getAllByRole("listitem")`. We can then get the length of the returned array to check how many items there are.

In more complicated apps, you may need not be able to find the elements you are after by searching by text, label or role. If there is no way around it, you can reach for [`querySelector()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) on the document body like you would in vanilla JavaScript on a regular webpage. Just try to avoid using this 'escape hatch' if possible.

Some people may choose to defer some of the testing of the `App` component to end-to-end testing. It depends on who you are working with, and how the project is organized to decide who tests what, and where.

---

And that's the bits that I think stand out the most, you can read through the tests yourself to get a more complete grasp.

The test coverage is 98%.

<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p4wq0pgerpjlp9yvoivl.png" alt="counter component" width="963" height="557" loading="lazy"/>

One important thing that I did not cover in my app is [Test Doubles](https://www.martinfowler.com/bliki/TestDouble.html). Even though it is quite a small app, I wrote what are called [*social tests*](https://www.martinfowler.com/bliki/UnitTest.html#SolitaryOrSociable). The alternate approach is *solitary tests*. For solitary tests, you need to mock components, you are trying to isolate a component and only the test the functionality of that "unit".

In both approaches, you may need to mock some functions that rely on third-party libraries or native browser APIs. One common example is mocking calls to backend services through [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) or [`axios`](https://axios-http.com/). I didn't use a backend service in my app, so I did not need to mock anything. This is something that I may pick up in another article.

## Conclusion

It is messy to get Jest set-up with Svelte and Vite. The template I have provided here will allow you start testing your Svelte components out of the gates. While you can get quite far without issues, using ESM in your frontend code and dev tools, but using a testing library that uses CommonJS, will inevitably create more work for you. I guess we will have to wait and see if Jest will make this simpler with its ESM support, and whether Vite will offer [first class Jest integration](https://github.com/vitejs/vite/issues/1955) some time soon.

I would like to find an alternative unit testing library that requires less configuration and integrates with Vite and Svelte in a more seamless way. I wonder if using a testing framework such as Jest that uses [jsdom](https://github.com/jsdom/jsdom), a virtual DOM implementation, under the hood is avoidable. If Svelte has ditched the virtual DOM, could the testing framework do the same? Getting closer to actual browser experience will make testing a bit more realistic too. This feels like a neglected aspect of the frontend dev stack evolution to me.

Regardless of the details, I encourage you to test your Svelte apps and make testing a core part of your development process. I hope I have shown that it is easier than you may think! The confidence that you will get from testing is invaluable to make more reliable and resilient apps. Don't treat it as an optional task for your own sake!