← Back to DevBytes

Testing Svelte Components: From Unit to E2E Tests

Testing Svelte Components: From Unit to E2E Tests

Testing is a critical part of building robust Svelte applications. Whether you're building a small widget or a large-scale application, having a comprehensive testing strategy ensures your components behave as expected, regressions are caught early, and refactoring becomes safer. In this tutorial, we'll explore the full testing spectrum for Svelte components — from isolated unit tests to end-to-end (E2E) tests that simulate real user interactions.

Why Testing Svelte Components Matters

Svelte's compiler-based approach makes it incredibly easy to build reactive, performant UIs. However, this same reactivity can introduce subtle bugs that are hard to catch manually. For example, a store update might trigger unexpected side effects, or a component might not clean up its event listeners properly. Automated tests help you catch these issues before they reach production.

Here are the key benefits of testing Svelte components:

The Testing Pyramid for Svelte

Before diving into code, it's important to understand the testing pyramid and how it applies to Svelte applications:

You should have many unit and component tests, fewer integration tests, and a small number of E2E tests. This balance keeps your test suite fast while still providing comprehensive coverage.

Setting Up the Testing Environment

For modern Svelte testing, the recommended stack is Vitest for unit and component testing, @testing-library/svelte for component rendering and interaction, and Playwright for E2E testing. Let's set up the environment step by step.

Installing Dependencies

If you're using a Vite-based Svelte project (created with npm create svelte@latest), you can install the testing dependencies as follows:

npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom
npm install -D @playwright/test
npx playwright install

Configuring Vitest

Create or update your vite.config.js file to include Vitest configuration:

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.js'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

Creating the Test Setup File

Create a tests/setup.js file to configure custom matchers from @testing-library/jest-dom:

import '@testing-library/jest-dom/vitest';

This setup gives you access to helpful DOM matchers like toBeInTheDocument(), toHaveTextContent(), and toBeVisible() in your tests.

Unit Testing Svelte Stores and Logic

Unit tests focus on testing individual pieces of logic in isolation. In Svelte applications, this often means testing stores and utility functions. Let's start with a simple example.

Testing a Svelte Store

Suppose you have a counter store defined in src/stores/counter.js:

import { writable } from 'svelte/store';

export function createCounter(initialValue = 0) {
  const { subscribe, set, update } = writable(initialValue);

  return {
    subscribe,
    increment: () => update((n) => n + 1),
    decrement: () => update((n) => n - 1),
    reset: () => set(initialValue),
  };
}

Here's how you would write unit tests for this store:

import { describe, it, expect } from 'vitest';
import { get } from 'svelte/store';
import { createCounter } from '../src/stores/counter';

describe('createCounter', () => {
  it('initializes with the given value', () => {
    const counter = createCounter(5);
    expect(get(counter)).toBe(5);
  });

  it('increments the value', () => {
    const counter = createCounter(0);
    counter.increment();
    expect(get(counter)).toBe(1);
  });

  it('decrements the value', () => {
    const counter = createCounter(5);
    counter.decrement();
    expect(get(counter)).toBe(4);
  });

  it('resets to the initial value', () => {
    const counter = createCounter(10);
    counter.increment();
    counter.increment();
    counter.reset();
    expect(get(counter)).toBe(10);
  });
});

These tests are fast, isolated, and verify the core logic of the store without any DOM involvement.

Testing Utility Functions

Utility functions are the easiest to unit test. For example, if you have a formatting utility:

// src/utils/format.js
export function formatCurrency(amount, currency = 'USD') {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount);
}

export function truncate(text, maxLength = 50) {
  if (text.length <= maxLength) return text;
  return text.slice(0, maxLength - 3) + '...';
}

The corresponding test file would be:

import { describe, it, expect } from 'vitest';
import { formatCurrency, truncate } from '../src/utils/format';

describe('formatCurrency', () => {
  it('formats a number as USD by default', () => {
    expect(formatCurrency(1234.56)).toBe('$1,234.56');
  });

  it('formats a number with a custom currency', () => {
    expect(formatCurrency(1234.56, 'EUR')).toBe('€1,234.56');
  });

  it('handles zero', () => {
    expect(formatCurrency(0)).toBe('$0.00');
  });
});

describe('truncate', () => {
  it('returns the original text if shorter than max length', () => {
    expect(truncate('Hello', 50)).toBe('Hello');
  });

  it('truncates long text with ellipsis', () => {
    const longText = 'This is a very long text that needs to be truncated';
    expect(truncate(longText, 20)).toBe('This is a very lo...');
  });
});

Component Testing with @testing-library/svelte

Component testing is where things get interesting. @testing-library/svelte allows you to render Svelte components in a simulated DOM environment and interact with them as a user would. The library's guiding principle is: "The more your tests resemble the way your software is used, the more confidence they can give you."

Testing a Simple Component

Let's start with a simple Button component:

<!-- src/lib/Button.svelte -->
<script>
  export let label = 'Click me';
  export let disabled = false;
  export let variant = 'primary';

  function handleClick(event) {
    if (disabled) return;
    dispatch('click', event);
  }

  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();
</script>

<button
  class="btn btn-{variant}"
  {disabled}
  on:click={handleClick}
>
  {label}
</button>

Here's how you would test this component:

import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import Button from '../src/lib/Button.svelte';

describe('Button', () => {
  it('renders with default label', () => {
    const { getByRole } = render(Button);
    expect(getByRole('button')).toHaveTextContent('Click me');
  });

  it('renders with a custom label', () => {
    const { getByRole } = render(Button, { props: { label: 'Submit' } });
    expect(getByRole('button')).toHaveTextContent('Submit');
  });

  it('applies the correct variant class', () => {
    const { getByRole } = render(Button, { props: { variant: 'danger' } });
    expect(getByRole('button')).toHaveClass('btn-danger');
  });

  it('dispatches a click event when clicked', async () => {
    const { getByRole, component } = render(Button);
    const mockHandler = vi.fn();
    component.$on('click', mockHandler);

    await fireEvent.click(getByRole('button'));
    expect(mockHandler).toHaveBeenCalledTimes(1);
  });

  it('does not dispatch click when disabled', async () => {
    const { getByRole, component } = render(Button, {
      props: { disabled: true },
    });
    const mockHandler = vi.fn();
    component.$on('click', mockHandler);

    await fireEvent.click(getByRole('button'));
    expect(mockHandler).not.toHaveBeenCalled();
  });
});

Testing Component Props and Reactivity

Svelte's reactivity is one of its most powerful features. Let's test a component that reacts to prop changes. Consider a Greeting component:

<!-- src/lib/Greeting.svelte -->
<script>
  export let name = 'World';
  export let count = 0;
</script>

<h1>Hello, {name}!</h1>
<p>You have visited {count} {count === 1 ? 'time' : 'times'}.</p>

Testing reactivity involves updating component props and verifying the DOM updates:

import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/svelte';
import Greeting from '../src/lib/Greeting.svelte';

describe('Greeting', () => {
  it('renders with default props', () => {
    const { getByText } = render(Greeting);
    expect(getByText('Hello, World!')).toBeInTheDocument();
    expect(getByText('You have visited 0 times.')).toBeInTheDocument();
  });

  it('updates when props change', async () => {
    const { getByText, rerender } = render(Greeting, {
      props: { name: 'Alice', count: 1 },
    });

    expect(getByText('Hello, Alice!')).toBeInTheDocument();
    expect(getByText('You have visited 1 time.')).toBeInTheDocument();

    await rerender({ props: { name: 'Bob', count: 5 } });

    expect(getByText('Hello, Bob!')).toBeInTheDocument();
    expect(getByText('You have visited 5 times.')).toBeInTheDocument();
  });
});

Testing Components with Slots

Testing components that use slots requires passing slot content during rendering. Here's a Card component:

<!-- src/lib/Card.svelte -->
<script>
  export let title = '';
</script>

<div class="card">
  {#if title}
    <h2 class="card-title">{title}</h2>
  {/if}
  <div class="card-body">
    <slot />
  </div>
</div>

To test the slot, you can use the props option with a special $$slots approach or wrap the component:

import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/svelte';
import Card from '../src/lib/Card.svelte';

describe('Card', () => {
  it('renders title and slot content', () => {
    const { getByText, container } = render(Card, {
      props: { title: 'My Card' },
    });

    expect(getByText('My Card')).toBeInTheDocument();
    expect(container.querySelector('.card-body')).toBeInTheDocument();
  });

  it('does not render title when not provided', () => {
    const { container } = render(Card);
    expect(container.querySelector('.card-title')).not.toBeInTheDocument();
  });
});

Testing Components with Stores

Many Svelte components rely on stores for state management. Testing these components requires careful handling of store state. Let's look at a TodoList component that uses a store:

<!-- src/lib/TodoList.svelte -->
<script>
  import { todos, addTodo, toggleTodo, removeTodo } from '../stores/todos.js';

  let newTodoText = '';

  function handleSubmit(event) {
    event.preventDefault();
    if (newTodoText.trim()) {
      addTodo(newTodoText.trim());
      newTodoText = '';
    }
  }
</script>

<form on:submit={handleSubmit}>
  <input type="text" bind:value={newTodoText} placeholder="Add a todo..." />
  <button type="submit">Add</button>
</form>

<ul>
  {#each $todos as todo (todo.id)}
    <li class={todo.completed ? 'completed' : ''}>
      <input
        type="checkbox"
        checked={todo.completed}
        on:change={() => toggleTodo(todo.id)}
      />
      <span>{todo.text}</span>
      <button on:click={() => removeTodo(todo.id)}>Delete</button>
    </li>
  {/each}
</ul>

The store definition:

// src/stores/todos.js
import { writable } from 'svelte/store';

function createTodoStore() {
  const { subscribe, update } = writable([]);

  return {
    subscribe,
    addTodo: (text) => update((todos) => [
      ...todos,
      { id: Date.now(), text, completed: false },
    ]),
    toggleTodo: (id) => update((todos) =>
      todos.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    ),
    removeTodo: (id) => update((todos) =>
      todos.filter((t) => t.id !== id)
    ),
  };
}

export const todos = createTodoStore();
export const { addTodo, toggleTodo, removeTodo } = todos;

Testing the component with the store:

import { describe, it, expect, beforeEach } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import TodoList from '../src/lib/TodoList.svelte';
import { todos, addTodo } from '../src/stores/todos.js';
import { get } from 'svelte/store';

describe('TodoList', () => {
  beforeEach(() => {
    // Reset the store before each test
    todos.set([]);
  });

  it('renders an empty list initially', () => {
    const { container } = render(TodoList);
    expect(container.querySelector('ul').children).toHaveLength(0);
  });

  it('adds a new todo when form is submitted', async () => {
    const { getByPlaceholderText, getByText, container } = render(TodoList);

    const input = getByPlaceholderText('Add a todo...');
    await fireEvent.input(input, { target: { value: 'Buy groceries' } });
    await fireEvent.click(getByText('Add'));

    expect(container.querySelector('ul').children).toHaveLength(1);
    expect(getByText('Buy groceries')).toBeInTheDocument();
  });

  it('toggles todo completion when checkbox is clicked', async () => {
    addTodo('Test todo');
    const { getByRole, container } = render(TodoList);

    const checkbox = getByRole('checkbox');
    expect(checkbox).not.toBeChecked();

    await fireEvent.click(checkbox);
    expect(checkbox).toBeChecked();
    expect(container.querySelector('li')).toHaveClass('completed');
  });

  it('removes a todo when delete button is clicked', async () => {
    addTodo('Todo to delete');
    const { getByText, container } = render(TodoList);

    await fireEvent.click(getByText('Delete'));
    expect(container.querySelector('ul').children).toHaveLength(0);
  });
});

Mocking Dependencies in Component Tests

Sometimes your components depend on external modules, API calls, or other components. Vitest provides powerful mocking capabilities. Here's an example of mocking an API call:

// src/lib/UserProfile.svelte
<script>
  import { onMount } from 'svelte';
  import { fetchUser } from '../api/users.js';

  let user = null;
  let loading = true;
  let error = null;

  onMount(async () => {
    try {
      user = await fetchUser(1);
    } catch (e) {
      error = e.message;
    } finally {
      loading = false;
    }
  });
</script>

{#if loading}
  <p data-testid="loading">Loading...</p>
{:else if error}
  <p data-testid="error">Error: {error}</p>
{:else if user}
  <div data-testid="profile">
    <h2>{user.name}</h2>
    <p>{user.email}</p>
  </div>
{/if}

Testing with a mocked API:

import { describe, it, expect, vi } from 'vitest';
import { render, waitFor } from '@testing-library/svelte';
import UserProfile from '../src/lib/UserProfile.svelte';

vi.mock('../src/api/users.js', () => ({
  fetchUser: vi.fn(),
}));

import { fetchUser } from '../src/api/users.js';

describe('UserProfile', () => {
  it('displays user data when API call succeeds', async () => {
    fetchUser.mockResolvedValue({
      id: 1,
      name: 'Jane Doe',
      email: 'jane@example.com',
    });

    const { getByTestId, getByText } = render(UserProfile);

    expect(getByTestId('loading')).toBeInTheDocument();

    await waitFor(() => {
      expect(getByTestId('profile')).toBeInTheDocument();
    });

    expect(getByText('Jane Doe')).toBeInTheDocument();
    expect(getByText('jane@example.com')).toBeInTheDocument();
  });

  it('displays error message when API call fails', async () => {
    fetchUser.mockRejectedValue(new Error('Network error'));

    const { getByTestId } = render(UserProfile);

    await waitFor(() => {
      expect(getByTestId('error')).toBeInTheDocument();
    });

    expect(getByTestId('error')).toHaveTextContent('Error: Network error');
  });
});

Testing Svelte Components with Lifecycle Hooks

Svelte components have lifecycle hooks like onMount, onDestroy, and tick. Testing these requires understanding when they fire. Here's an example component that sets up an interval on mount:

<!-- src/lib/Timer.svelte -->
<script>
  import { onMount, onDestroy } from 'svelte';

  let seconds = 0;
  let interval;

  onMount(() => {
    interval = setInterval(() => {
      seconds += 1;
    }, 1000);
  });

  onDestroy(() => {
    if (interval) clearInterval(interval);
  });
</script>

<p data-testid="timer">{seconds} seconds elapsed</p>

Testing with fake timers:

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render } from '@testing-library/svelte';
import Timer from '../src/lib/Timer.svelte';

describe('Timer', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('starts at zero', () => {
    const { getByTestId } = render(Timer);
    expect(getByTestId('timer')).toHaveTextContent('0 seconds elapsed');
  });

  it('increments every second', async () => {
    const { getByTestId } = render(Timer);

    vi.advanceTimersByTime(3000);

    // Wait for Svelte's reactivity to flush
    await new Promise((resolve) => setTimeout(resolve, 0));

    expect(getByTestId('timer')).toHaveTextContent('3 seconds elapsed');
  });
});

Integration Testing Multiple Components

Integration tests verify that multiple components work together correctly. Let's test a small application that combines several components. Consider a shopping cart feature:

<!-- src/lib/ShoppingCart.svelte -->
<script>
  import ProductList from './ProductList.svelte';
  import CartSummary from './CartSummary.svelte';
  import { cart } from '../stores/cart.js';

  function handleAddToCart(product) {
    cart.addItem(product);
  }
</script>

<div class="shopping-cart">
  <ProductList on:addtocart={(e) => handleAddToCart(e.detail)} />
  <CartSummary />
</div>

Testing the integration:

import { describe, it, expect, beforeEach } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import ShoppingCart from '../src/lib/ShoppingCart.svelte';
import { cart } from '../src/stores/cart.js';

describe('ShoppingCart integration', () => {
  beforeEach(() => {
    cart.clear();
  });

  it('adds products to cart and updates summary', async () => {
    const { getByText, getByTestId } = render(ShoppingCart);

    // Initially cart is empty
    expect(getByTestId('cart-count')).toHaveTextContent('0');
    expect(getByTestId('cart-total')).toHaveTextContent('$0.00');

    // Add a product
    await fireEvent.click(getByText('Add to Cart'));

    // Cart should update
    expect(getByTestId('cart-count')).toHaveTextContent('1');
    expect(getByTestId('cart-total')).toHaveTextContent('$29.99');
  });

  it('adds multiple products and calculates total', async () => {
    const { getAllByText, getByTestId } = render(ShoppingCart);

    const addButtons = getAllByText('Add to Cart');
    await fireEvent.click(addButtons[0]);
    await fireEvent.click(addButtons[1]);

    expect(getByTestId('cart-count')).toHaveTextContent('2');
  });
});

End-to-End Testing with Playwright

While unit and component tests verify individual pieces, E2E tests verify entire user flows in a real browser. Playwright is the recommended tool for E2E testing Svelte applications because it's fast, reliable, and has excellent developer experience.

Configuring Playwright

Create a playwright.config.js file:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
  },
});

Writing Your First E2E Test

Let's write an E2E test for a login flow. Assume your Svelte app has a login page at /login:

// tests/e2e/login.spec.js
import { test, expect } from '@playwright/test';

test.describe('Login flow', () => {
  test('successfully logs in with valid credentials', async ({ page }) => {
    await page.goto('/login');

    // Fill in the login form
    await page.fill('[data-testid="email-input"]', 'user@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');

    // Submit the form
    await page.click('[data-testid="login-button"]');

    // Verify redirect to dashboard
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('[data-testid="welcome-message"]')).toContainText(
      'Welcome back'
    );
  });

  test('shows error with invalid credentials', async ({ page }) => {
    await page.goto('/login');

    await page.fill('[data-testid="email-input"]', 'wrong@example.com');
    await page.fill('[data-testid="password-input"]', 'wrongpassword');
    await page.click('[data-testid="login-button"]');

    await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
    await expect(page.locator('[data-testid="error-message"]')).toContainText(
      'Invalid credentials'
    );
  });

  test('validates required fields', async ({ page }) => {
    await page.goto('/login');

    await page.click('[data-testid="login-button"]');

    await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
    await expect(page.locator('[data-testid="password-error"]')).toBeVisible();
  });
});

Testing a Complete Shopping Flow

Here's a more complex E2E test that covers a full shopping experience:

// tests/e2e/shopping.spec.js
import { test, expect } from '@playwright/test';

test.describe('Shopping flow', () => {
  test('browse, add to cart, and checkout', async ({ page }) => {
    // Navigate to products page
    await page.goto('/products');

    // Wait for products to load
    await expect(page.locator('[data-testid="product-card"]')).toHaveCount(6);

    // Add first product to cart
    await page.locator('[data-testid="product-card"]').first()
      .locator('button:has-text("Add to Cart")').click();

    // Verify cart badge updates
    await expect(page.locator('[data-testid="cart-badge"]')).toHaveText('1');

    // Go to cart page
    await page.click('[data-testid="cart-link"]');

    // Verify product is in cart
    await expect(page.locator('[data-testid="cart-item"]')).toHaveCount(1);

    // Proceed to checkout
    await page.click('[data-testid="checkout-button"]');

    // Fill in shipping information
    await page.fill('[data-testid="name-input"]', 'John Doe');
    await page.fill('[data-testid="address-input"]', '123 Main St');
    await page.fill('[data-testid="city-input"]', 'Anytown');
    await page.fill('[data-testid="zip-input"]', '12345');

    // Place order
    await page.click('[data-testid="place-order-button"]');

    // Verify order confirmation
    await expect(page).toHaveURL('/order-confirmation');
    await expect(page.locator('[data-testid="order-number"]')).toBeVisible();
  });

  test('can remove items from cart', async ({ page }) => {
    await page.goto('/products');

    // Add two products
    await page.locator('[data-testid="product-card"]').nth(0)
      .locator('button:has-text("Add to Cart")').click();
    await page.locator('[data-testid="product-card"]').nth(1)
      .locator('button:has-text("Add to Cart")').click();

    // Go to cart
    await page.click('[data-testid="cart-link"]');

    // Remove first item
    await page.locator('[data-testid="cart-item"]').first()
      .locator('[data-testid="remove-item"]').click();

    // Verify only one item remains
    await expect(page.locator('[data-testid="cart-item"]')).toHaveCount(1);
  });
});

Testing SvelteKit Pages

If you're using SvelteKit, you'll want to test page routing and server-side features. Playwright handles this naturally since it tests against a running server:

// tests/e2e/navigation.spec.js
import { test, expect } from '@playwright/test';

test.describe('Navigation', () => {
  test('can navigate between pages using links', async ({ page }) => {
    await page.goto('/');

    // Click on the About link
    await page.click('a:has-text("About")');
    await expect(page).toHaveURL('/about');
    await expect(page.locator('h1')).toHaveText('About Us');

    // Navigate to Contact
    await page.click('a:has-text("Contact")');
    await expect(page).toHaveURL('/contact');
  });

  test('handles 404 for unknown routes', async ({ page }) => {
    await page.goto('/nonexistent-page');
    await expect(page.locator('[data-testid="not-found"]')).toBeVisible();
  });

  test('preserves state when navigating back', async ({ page }) => {
    await page.goto('/search');
    await page.fill('[data-testid="search-input"]', 'svelte testing');
    await page.click('[data-testid="search-button"]');

    // Navigate away
    await page.click('a:has-text("Home")');

    // Navigate back
    await page.goBack();

    // Search input should still have the value (if using SvelteKit's state preservation)
    await expect(page.locator('[data-testid="search-input"]')).toHaveValue(
      'svelte testing'
    );
  });
});

Testing Svelte Transitions and Animations

Svelte's transition directives can be tricky to test because they involve CSS animations. The key is to test the state before and after the transition rather than the animation itself:

<!-- src/lib/Modal.svelte -->
<script>
  import { fade } from 'svelte/transition';
  export let open = false;
</script>

{#if open}
  <div class="modal-overlay" transition:fade on:click|self={() => (open = false)}>
    <div class="modal-content" data-testid="modal-content">
      <slot />
      <button data-testid="modal-close" on:click={() => (open = false)}>
        Close
      </button>
    </div>
  </div>
{/if}

Testing the modal:

import { describe, it, expect } from 'vitest';
import { render, fireEvent } from '@testing-library/svelte';
import Modal from '../src/lib/Modal.svelte';

describe('Modal', () => {
  it('does not render when open is false', () => {
    const { queryByTestId } = render(Modal, { props: { open: false } });
    expect(queryByTestId('modal-content')).not.toBeInTheDocument();
  });

  it('renders when open is true', () => {
    const { getByTestId } = render(Modal, { props: { open: true } });
    expect(getByTestId('modal-content')).toBeInTheDocument();
  });

  it('closes when close button is clicked', async () => {
    const { getByTestId, queryByTestId, component } = render(Modal, {
      props: { open: true },
    });

    await fireEvent.click(getByTestId('modal-close'));

    // The component's open prop should be updated
    expect(component.open).toBe(false);
  });
});

Best Practices for Testing Svelte Components

1. Test Behavior, Not Implementation

Avoid testing internal implementation details that are likely to change. Instead, focus on what the component does from the user's perspective. Use semantic queries like getByRole, getByLabelText, and getByText rather than querying by class names or DOM structure.

// ❌ Bad: Testing implementation details
expect(component.querySelector('.internal-class-name')).toBeTruthy();

// ✅ Good: Testing user-visible behavior
expect(getByRole('button', { name: 'Submit' })).toBeVisible();

2. Use data-testid for Unstable Elements

When text content or roles are dynamic or ambiguous, use data-testid attributes to create stable selectors:

<button data-testid="submit-order">Place Order</button>

3. Keep Tests Isolated

Each test should be independent and not rely on the state from previous tests. Use beforeEach to reset stores and mock state:

beforeEach(() => {
  cart.clear();
  vi.clearAllMocks();
});

4. Test Accessibility

Include accessibility checks in your tests. This ensures your components are usable by everyone:

import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/svelte';
import FormField from '../src/lib/FormField.svelte';

describe('FormField accessibility', () => {
  it('associates label with input', () => {
    const { getByLabelText } = render(FormField, {
      props: { label: 'Email', id: 'email' },
    });

    const input = getByLabelText('Email');
    expect(input).toHaveAttribute('type', 'email');
  });

  it('marks required fields with aria-required', () => {
    const { getByLabelText } = render(FormField, {
      props: { label: 'Name', id: 'name', required: true },
    });

    expect(getByLabelText('Name')).toHaveAttribute('aria

— Ad —

Google AdSense will appear here after approval

← Back to all articles