- 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.
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.
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.
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"
}
}
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`
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
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();
}
}
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();
}
}
}