[go: up one dir, main page]

DEV Community

Cover image for Component Testing in Svelte
Ignacio Le Fluk for This Dot

Posted on • Originally published at thisdot.co

Component Testing in Svelte

Testing helps us trust our application, and it's a safety net for future changes. In this tutorial, we will set up our Svelte project to run tests for our components.

Starting a new project

Let's start by creating a new project:

pnpm dlx create-vite
// Project name: › testing-svelte
// Select a framework: › svelte
// Select a variant: › svelte-ts

cd testing-svelte
pnpm install
Enter fullscreen mode Exit fullscreen mode

There are other ways of creating a Svelte project, but I prefer using Vite. One of the reasons that I prefer using Vite is that SvelteKit will use it as well. I'm also a big fan of pnpm, but you can use your preferred package manager. Make sure you follow Vite's docs on starting a new project using npm or yarn.

Installing required dependencies

  • Jest: I'll be using this framework for testing. It's the one that I know best, and feel more comfortable with. Because I'm using TypeScript, I need to install its type definitions too.
  • ts-jest: A transformer for handling TypeScript files.
  • svelte-jester: precompiles Svelte components before tests.
  • Testing Library: Doesn't matter what framework I'm using, I will look for an implementation of this popular library.
pnpm install --save-dev jest @types/jest @testing-library/svelte svelte-jester ts-jest
Enter fullscreen mode Exit fullscreen mode

Configuring tests

Now that our dependencies are installed, we need to configure jest to prepare the tests and run them.

A few steps are required:

  • Convert *.ts files
  • Complile *.svelte files
  • Run the tests

Create a configuration file at the root of the project:

// jest.config.js
export default {
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.svelte$': [
      'svelte-jester',
      {
        preprocess: true,
      },
    ],
  },
  moduleFileExtensions: ['js', 'ts', 'svelte'],
};

Enter fullscreen mode Exit fullscreen mode

Jest will now use ts-jest for compiling *.ts files, and svelte-jester for *.svelte files.

Creating a new test

Let's test the Counter component created when we started the project, but first, I'll check what our component does.

<script lang="ts">
  let count: number = 0;
  const increment = () => {
    count += 1;
  };
</script>

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

<style>
  button {
    font-family: inherit;
    font-size: inherit;
    padding: 1em 2em;
    color: #ff3e00;
    background-color: rgba(255, 62, 0, 0.1);
    border-radius: 2em;
    border: 2px solid rgba(255, 62, 0, 0);
    outline: none;
    width: 200px;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
  }

  button:focus {
    border: 2px solid #ff3e00;
  }

  button:active {
    background-color: rgba(255, 62, 0, 0.2);
  }
</style>
Enter fullscreen mode Exit fullscreen mode

This is a very small component where a button when clicked, updates a count, and that count is reflected in the button text.
So, that's exactly what we'll be testing.

I'll create a new file ./lib/__tests__/Counter.spec.ts

/**
 * @jest-environment jsdom
 */

import { render, fireEvent } from '@testing-library/svelte';
import Counter from '../Counter.svelte';

describe('Counter', () => {
  it('it changes count when button is clicked', async () => {
    const { getByText } = render(Counter);
    const button = getByText(/Clicks:/);
    expect(button.innerHTML).toBe('Clicks: 0');
    await fireEvent.click(button);
    expect(button.innerHTML).toBe('Clicks: 1');
  });
});
Enter fullscreen mode Exit fullscreen mode

We are using render and fireEvent from testing-library. Be mindful that fireEvent returns a Promise and we need to await for it to be fulfilled.
I'm using the getByText query, to get the button being clicked.
The comment at the top, informs jest that we need to use jsdom as the environment. This will make things like document available, otherwise, render will not be able to mount the component. This can be set up globally in the configuration file.

What if we wanted, to test the increment method in our component?
If it's not an exported function, I'd suggest testing it through the rendered component itself. Otherwise, the best option is to extract that function to another file, and import it into the component.

Let's see how that works.

// lib/increment.ts
export function increment (val: number) {
    val += 1;
    return val
  };
Enter fullscreen mode Exit fullscreen mode
<!-- lib/Counter.svelte -->
<script lang="ts">
  import { increment } from './increment';
  let count: number = 0;
</script>

<button on:click={() => (count = increment(count))}>
  Clicks: {count}
</button>
<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

Our previous tests will still work, and we can add a test for our function.

// lib/__tests__/increment.spec.ts

import { increment } from '../increment';

describe('increment', () => {
  it('it returns value+1 to given value when called', async () => {
    expect(increment(0)).toBe(1);
    expect(increment(-1)).toBe(0);
    expect(increment(1.2)).toBe(2.2);
  });
});
Enter fullscreen mode Exit fullscreen mode

In this test, there's no need to use jsdom as the test environment. We are just testing the function.

If our method was exported, we can then test it by accessing it directly.

<!-- lib/Counter.svelte -->
<script lang="ts">
  let count: number = 0;
  export const increment = () => {
    count += 1;
  };
</script>

<button on:click={increment}>
  Clicks: {count}
</button>
<!-- ... -->
Enter fullscreen mode Exit fullscreen mode
// lib/__tests__/Counter.spec.ts

describe('Counter Component', () => {
 // ... other tests

  describe('increment', () => {
    it('it exports a method', async () => {
      const { component } = render(Counter);
      expect(component.increment).toBeDefined();
    });

    it('it exports a method', async () => {
      const { getByText, component } = render(Counter);
      const button = getByText(/Clicks:/);
      expect(button.innerHTML).toBe('Clicks: 0');
      await component.increment()
      expect(button.innerHTML).toBe('Clicks: 1');
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

When the method is exported, you can access it directly from the returned component property of the render function.

NOTE: I don't recommend exporting methods from the component for simplicity if they were not meant to be exported. This will make them available from the outside, and callable from other components.

Events

If your component dispatches an event, you can test it using the component property returned by render.

To dispatch an event, we need to import and call createEventDispatcher, and then call the returning funtion, giving it an event name and an optional value.

<!-- lib/Counter.svelte -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  let count: number = 0;
  export const increment = () => {
    count += 1;
    dispatch('countChanged', count);
  };
</script>

<button on:click={increment}>
  Clicks: {count}
</button>
<!-- ... -->
Enter fullscreen mode Exit fullscreen mode
// lib/__tests__/Counter.spec.ts
// ...

  it('it emits an event', async () => {
    const { getByText, component } = render(Counter);
    const button = getByText(/Clicks:/);
    let mockEvent = jest.fn();
    component.$on('countChanged', function (event) {
      mockEvent(event.detail);
    });
    await fireEvent.click(button);

    // Some examples on what to test
    expect(mockEvent).toHaveBeenCalled(); // to check if it's been called
    expect(mockEvent).toHaveBeenCalledTimes(1); // to check how any times it's been called
    expect(mockEvent).toHaveBeenLastCalledWith(1); // to check the content of the event
    await fireEvent.click(button);
    expect(mockEvent).toHaveBeenCalledTimes(2);
    expect(mockEvent).toHaveBeenLastCalledWith(2);
  });

//...
Enter fullscreen mode Exit fullscreen mode

For this example, I updated the component to emit an event: countChanged. Every time the button is clicked, the event will emit the new count.
In the test, I'm using getByText to select the button to click, and component.

Then, I'm using component.$on(eventName), and mocking the callback function to test the emitted value (event.detail).

Props

You can set initial props values, and modifying them using the client-side component API.

Let's update our component to receive the initial count value.

<!-- lib/Counter.svelte -->
<script lang="ts">
// ...
  export let count: number = 0;
// ...
</script>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

Converting count to an input value requires exporting the variable declaration.

Then we can test:

  • default values
  • initial values
  • updating values
// lib/__tests__/Counter.ts
// ...
describe('count', () => {
    it('defaults to 0', async () => {
      const { getByText } = render(Counter);
      const button = getByText(/Clicks:/);
      expect(button.innerHTML).toBe('Clicks: 0');
    });

    it('can have an initial value', async () => {
      const { getByText } = render(Counter, {props: {count: 33}});
      const button = getByText(/Clicks:/);
      expect(button.innerHTML).toBe('Clicks: 33');
    });

    it('can be updated', async () => {
      const { getByText, component } = render(Counter);
      const button = getByText(/Clicks:/);
      expect(button.innerHTML).toBe('Clicks: 0');
      await component.$set({count: 41})
      expect(button.innerHTML).toBe('Clicks: 41');
    });
});
// ...
Enter fullscreen mode Exit fullscreen mode

We are using the second argument of the render method to pass initial values to count, and we are testing it through the rendered button

To update the value, we are calling the $set method on component, which will update the rendered value on the next tick. That's why we need to await it.

Wrapping up

Testing components using Jest and Testing Library can help you avoid errors when developing, and also can make you more confident when applying changes to an existing codebase. I hope this blog post is a step forward to better testing.

You can find these examples in this repo


This Dot Labs is a development consultancy focused on providing staff augmentation, architectural guidance, and consulting to companies.

We help implement and teach modern web best practices with technologies such as React, Angular, Vue, Web Components, GraphQL, Node, and more.

Top comments (0)