Jest was, is and probably will be the most popular testing library for Frontend projects. That was my thought when I went in and tried to setup Jest for a Preact project that we were working on, and ultimately giving up after 1 day of facing countless issues. The out-of-the-box jest setup mentioned in the docs doesn’t work for me, and having to introduced additional libraries like babel, ts-jest, while having to config jest to support css|scss|images and still, I could make it work.

That’s when vitest comes to my rescue.

  1. Setting up the required libraries:

  • vitest

  • @testing-library/preact

  • jsdom

We can add an E2E testing library like `Cypress` or `Playwright` later, but let’s keep things simple for now.

  1. Setup `vitest.config.ts` file in the root directory. Vite users can just use `vite.config.ts`, remember to change your `defineConfig` import from:

import { defineConfig } from 'vite';

to

import { defineConfig } from 'vitest/config';

**Important** Merge the config from vite.config.ts if you use vite and have a separate `vitest.config.ts` file.

  1. Writing base configs

//vite.config.ts

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true, // Enable global APIs
    environment: 'jsdom', // Enable DOM APIs. Required for React to work.
  },
  // ... other vite configs
});
// packages.json {    
    // … other configs    
    “scripts”: {        
        "test": "vitest"
    }
}
  1. Start writing unit tests.

// helpers.ts
export function sum(a: number, b: number) {
    return a + b;
}

// helpers.test.ts
import {expect, test, describe} from 'vitest';
import {sum} from "./helpers";

describe("Expect sum function", () => {
    test("to return a sum of 2 numbers", () => {
        const result = sum(1, 4);
        expect(result).toEqual(5);
    }
}

You can then run the test using `npm run test`

  1. Writing component tests.

// TestComponent.tsx
export function TestComponent() {
    return (
        
Hello World
) } // TestComponent.test.tsx import {expect, test, describe} from 'vitest'; import {TestComponent} from "./TestComponent"; import {render} from '@testing-library/preact'; describe("Expect Test Component", () => { test("to contain the word 'Hello'", () => { const {getByText} = render(); expect(getByText(/Hello/)).not.toBeNull(); } }

See Testing Library docs for more advanced testing options

  1. Mocking functions

There are cases when you want to mock a function (e.g. when this function interacts with cookies/database/APIs/DOM) to test a certain behaviour. For example:

// localStorage.ts
// Assuming the locale data is stored in local storagefunction getLocaleData() {
    return localStorage.getItem("locale");
}

// TestComponent.tsx
export function TestComponent() {
    const locale = getLocaleData();
    return (
        
Current locale is {locale}
) } // TestComponent.test.tsx import {expect, test, describe, afterEach} from 'vitest'; import {TestComponent} from "./TestComponent"; import {render} from '@testing-library/preact'; /* Create a hoisted mock function which can be modified differently in each test. A normal mock function, once changed, will affect all other usages. https://github.com/vitest-dev/vitest/discussions/3589 */ const mocks = vi.hoisted(() => ({ getLocaleData: vi.fn(() => 'en-us'), })); // Reset the mock function’s returned value after each test afterEach(() => { mocks.getLocaleData.mockRestore(); }); // Mock the modulevi.mock('./localStorage.ts', async importOriginal => { const mod = await importOriginal() satisfies object; return {...mod, getLocaleData: mocks.getLocaleData}; }); describe("Expect Test Component", () => { mocks.getLocaleData.mockReturnValue("en-au"); test("to display the current locale", () => { const {getByText} = render(); expect(getByText(/en-au/)).not.toBeNull(); } }
  1. Mocking hooks using spy

Hooks are a bit more difficult to mock. One way we can do that is to utilize the `spyOn` functionality and provide a custom implementation of the hook

// useLocaleHook.ts
import {useState} from "react";

export function useLocaleHook() {
    const [locale, setLocale] = useState();

    return {locale, setLocale}
}

// TestComponent.tsx
export function TestComponent() {
    const {locale, setLocale} = useLocaleHook();
    return (
        
Current locale is {locale}
) } // TestComponent.test.tsx import {expect, test, describe, vi} from 'vitest'; import {TestComponent} from './TestComponent'; import * as useLocale from './useLocaleHook.ts'; import {fireEvent, render} from '@testing-library/preact'; describe("Expect Test Component", () => { const useLocaleHookSpy = vi.spyOn(useLocale, 'useLocaleHook'); test("to display the current locale", () => { useLocaleHookSpy.mockImplementation(() => { locale: 'en-ca', setLocale: () => {} } const {getByText} = render(); expect(getByText(/en-us/)).not.toBeNull(); } test("to be able to change the language", () => { const mockFn = vi.fn(() => {}); useLocaleHookSpy.mockImplementation(() => { locale: 'en-ca', setLocale: mockFn } const {container} = render(); const btn = container.querySelector('button'); expect(btn).to.notBeNull(); if (btn) { fireEvent.click(btn); expect(mockFn).toHaveBeenCalled(); } } }