Unit Testing with Vitest
Core Packages Versions
- vite: 5.4.10
- vitest: 3.0.5
- @vitest/coverage-v8: 3.0.5
- @vitest/ui: 3.0.5
Testing Components
We use @testing-library/react
, @testing-library/user-event
& @testing-library/jest-dom/vitest
with Vitest’s DOM integration for component testing. Our components follow the React Development Standards where we ensure:
- Core component functionality must be tested
- Focus on unit tests, not integration tests
- Keep tests alongside components (
component.test.tsx
)
Example Component Test:
import { render, screen } from "@testing-library/react";
import Button from "./Button";
describe("Button", () => {
it("renders with correct text", () => {
render(<Button>Click me</Button>);
expect(screen.getByText("Click me")).toBeInTheDocument();
});
it("handles disabled state", () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
// Calls a prop function when clicked
it("calls onButtonClick when clicked", async () => {
const handleClick = vi.fn();
render(<ExampleButton onButtonClick={handleClick} />);
const button = screen.getByRole("button");
await userEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("handles fetching RTK - API data on button click", async () => {
const mockFetchData = vi.fn().mockResolvedValue({});
vi.mocked(useFetchDataMutation).mockImplementation(() => [
mockFetchData,
{
isLoading: false,
reset: vi.fn(),
status: "idle",
isError: false,
isSuccess: false,
},
]);
customRender(<FetchDataButton />);
const button = screen.getByRole("button");
expect(button).not.toBeDisabled();
await userEvent.click(button);
await waitFor(() => expect(mockFetchData).toHaveBeenCalled(1));
});
});
Testing Hooks
For testing hooks, we use @testing-library/react
. This allows us to test custom hooks in isolation:
import { expect } from "vitest";
import { renderHook } from "@testing-library/react";
import { useCounter } from "./useCounter";
describe("useCounter hook", () => {
it("should increment counter", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
result.current.increment();
expect(result.current.count).toBe(1);
});
});
Testing RTK Slices
We test Redux Toolkit slices independently:
import { counterSlice, increment, decrement } from "./counterSlice";
describe("Counter Slice", () => {
it("should handle increment", () => {
const initialState = { value: 0 };
const nextState = counterSlice.reducer(initialState, increment());
expect(nextState.value).toBe(1);
});
it("should handle decrement", () => {
const initialState = { value: 1 };
const nextState = counterSlice.reducer(initialState, decrement());
expect(nextState.value).toBe(0);
});
});
Testing Views/Pages
We test view components focusing on their core rendering logic:
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import { store } from "../store";
import HomeView from "./HomeView";
describe("HomeView", () => {
it("renders main sections", () => {
render(
<Provider store={store}>
<HomeView />
</Provider>
);
expect(
screen.getByRole("heading", { name: /welcome/i })
).toBeInTheDocument();
expect(screen.getByTestId("main-content")).toBeInTheDocument();
});
});
Testing Store Actions
For testing store actions and reducers:
import { configureStore } from "@reduxjs/toolkit";
import authReducer, { setUser, logout } from "./authSlice";
describe("Auth Slice", () => {
let store;
beforeEach(() => {
store = configureStore({
reducer: { auth: authReducer },
});
});
it("should handle user login", () => {
const user = { id: 1, name: "Test User" };
store.dispatch(setUser(user));
const state = store.getState().auth;
expect(state.user).toEqual(user);
expect(state.isAuthenticated).toBe(true);
});
it("should handle logout", () => {
store.dispatch(logout());
const state = store.getState().auth;
expect(state.user).toBeNull();
expect(state.isAuthenticated).toBe(false);
});
});
Testing Zustand Store
For testing Zustand stores:
import { renderHook, act } from "@testing-library/react";
import useStore from "./store";
describe("Zustand Store", () => {
beforeEach(() => {
useStore.getState().reset();
});
it("should update theme", () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.setTheme("dark");
});
expect(result.current.theme).toBe("dark");
});
});
Testing Form Components
Example of testing form components with validation:
import { render, screen, fireEvent } from "@testing-library/react";
import LoginForm from "./LoginForm";
describe("LoginForm", () => {
it("shows validation errors for empty fields", async () => {
render(<LoginForm onSubmit={vi.fn()} />);
const submitButton = screen.getByRole("button", { name: /submit/i });
fireEvent.click(submitButton);
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
expect(
await screen.findByText(/password is required/i)
).toBeInTheDocument();
});
it("calls onSubmit with form data when valid", async () => {
const mockSubmit = vi.fn();
render(<LoginForm onSubmit={mockSubmit} />);
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: "test@example.com" },
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: "password123" },
});
fireEvent.click(screen.getByRole("button", { name: /submit/i }));
expect(mockSubmit).toHaveBeenCalledWith({
email: "test@example.com",
password: "password123",
});
});
});
Mocking Strategies
We use Vitest’s built-in mocking capabilities:
// Mocking RTK Query endpoints
vi.mock("@/common/services/custom.api", () => ({
useGetPointsQuery: vi.fn(() => ({
data: {
data: [],
},
isLoading: false,
isError: false,
isFetching: false,
refetch: vi.fn(),
})),
useGetPointsQuery: vi.fn(() => ({
data: {
data: {
records: [{ points: 100 }],
},
},
isLoading: false,
isError: false,
})),
useClaimPointsMutation: vi.fn(() => [
vi.fn().mockResolvedValue({}),
{
isLoading: false,
isError: false,
reset: vi.fn(),
status: "idle",
isSuccess: false,
},
]),
customApi: {
reducerPath: "custom",
reducer: () => ({}),
middleware: () => () => {},
},
}));
// Mocking modules
vi.mock("react-router-dom", () => ({
useNavigate: () => vi.fn(),
}));
Configure Store
import { MemoryRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import { customApi } from "@/common/services/custom.api";
const customRender = (ui: React.ReactElement) => {
const store = configureStore({
reducer: {
[customApi.reducerPath]: customApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(customApi.middleware),
});
return render(
<MemoryRouter>
<Provider store={store}>{ui}</Provider>
</MemoryRouter>
);
};
This example shows how to:
- Mock RTK Query hooks with their full response structure
- Mock query states (loading, error, etc.)
- Mock mutation hooks with their return tuple
- Mock API configuration objects
Test Utilities
We’ve created helper files in src/test-utils/
:
mockStore.ts
- Configures store for testingtestUtils.ts
- Common testing utilities
Running Tests
# Run all tests
pnpm run test
# or: vitest
# Generate coverage report
npm run coverage
# or: vitest run --coverage
# Vitest UI
pnpm run vitest:ui
# or: vitest --ui
Vitest UI
Vitest includes a powerful UI interface that helps visualize and debug tests. To use it:
pnpm run vitest:ui
# or: vitest --ui
Features
Test Explorer
- Tree view of all test files
- Real-time test execution status
- Filter and search capabilities
- Click to run individual tests
Test Details
- Detailed test results
- Stack traces for failures
- Test execution time
- Code coverage visualization
Console Output
- View console.log outputs
- Error messages and stack traces
- Test runtime information
Benefits
- Interactive debugging
- Real-time test feedback
- Visual code coverage
- Easy test filtering and search
- Better developer experience for test maintenance
Coverage Configuration
In vitest.config.js
:
export default defineConfig({
test: {
globals: true,
environment: "jsdom",
includeTaskLocation: true,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
include: ["src/**/*.{ts,tsx}"],
exclude: ["**/*.stories.tsx", "**/*.d.ts", "src/main.tsx", "**/index.ts"],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
});
Testing Standards
- Write unit tests for individual components and functions
- Test state management logic in isolation (RTK, Zustand, etc)
- Maintain high coverage thresholds
- Keep tests simple and focused