• Nhan Tran Blog
  • Posts
  • Simple Vitest + Preact setup for React component testing

Simple Vitest + Preact setup for React component testing

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 (
        <div>Hello World</div>
    )
}

// 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(<TestComponent />);
        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 (
        <div>Current locale is {locale}</div>
    )
}

// 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(<TestComponent />);
        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 (
        <div>
            Current locale is {locale}
            <button onClick={() => setLocale('en-au')}>Change locale</button>
        </div>
    )
}

// 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(<TestComponent />);
        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(<TestComponent />);
        const btn = container.querySelector('button');
        expect(btn).to.notBeNull();
        if (btn) {
             fireEvent.click(btn);
             expect(mockFn).toHaveBeenCalled();
        }
    }
}