Skip to content

Testing Setup & Guidelines

Overview

This project uses Vitest for unit testing with @testing-library/react for component testing. The setup includes MSW (Mock Service Worker) for API mocking and comprehensive test utilities.

Testing Stack

  • Test Runner: Vitest 4.0.17
  • Component Testing: @testing-library/react 16.3.1
  • User Interactions: @testing-library/user-event 14.6.1
  • API Mocking: MSW 2.12.7
  • Coverage: @vitest/coverage-v8 4.0.17
  • Environment: happy-dom 20.3.1

Project Structure

src/
โ”œโ”€โ”€ test/
โ”‚   โ”œโ”€โ”€ mocks/
โ”‚   โ”‚   โ”œโ”€โ”€ server.ts         # MSW server setup
โ”‚   โ”‚   โ”œโ”€โ”€ handlers.ts       # API request handlers
โ”‚   โ”œโ”€โ”€ test-utils.tsx        # Custom render function & providers
โ”‚   โ””โ”€โ”€ test-helpers.ts       # Utility functions for tests
โ”œโ”€โ”€ hooks/
โ”‚   โ”œโ”€โ”€ use-mobile.tsx
โ”‚   โ”œโ”€โ”€ use-mobile.test.tsx   # Hook tests
โ”‚   โ””โ”€โ”€ ...
โ””โ”€โ”€ components/
    โ””โ”€โ”€ common/
        โ”œโ”€โ”€ Tabs.tsx
        โ”œโ”€โ”€ Tabs.test.tsx     # Component tests
        โ””โ”€โ”€ ...

Running Tests

# Run tests in watch mode
bun run test

# Run tests once
bun run test:run

# Run tests with UI
bun run test:ui

# Generate coverage report
bun run test:coverage

Writing Tests

1. Hook Tests

import { describe, it, expect, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useMyHook } from '@/src/hooks/use-my-hook';

describe('useMyHook', () => {
  it('should initialize with default value', () => {
    const { result } = renderHook(() => useMyHook());
    expect(result.current).toBe(defaultValue);
  });

  it('should update state on action', () => {
    const { result } = renderHook(() => useMyHook());

    act(() => {
      result.current.doSomething();
    });

    expect(result.current.value).toBe(expectedValue);
  });
});

2. Component Tests

import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MyComponent } from '@/src/components/MyComponent';

describe('MyComponent', () => {
  it('should render component', () => {
    render(<MyComponent />);
    expect(screen.getByText('Expected text')).toBeInTheDocument();
  });

  it('should handle user interactions', async () => {
    const user = userEvent.setup();
    const handleClick = vi.fn();

    render(<MyComponent onClick={handleClick} />);
    await user.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalled();
  });
});

3. Service/Utility Tests

import { describe, it, expect, vi } from 'vitest';
import { myFunction } from '@/src/lib/my-utility';

vi.mock('@/src/services/apiClient', () => ({
  apiClient: {
    get: vi.fn(),
    post: vi.fn(),
  },
}));

describe('myFunction', () => {
  it('should return expected result', () => {
    const result = myFunction('input');
    expect(result).toBe('expected output');
  });
});

Mocking Strategies

API Requests (MSW)

The MSW server is automatically started for all tests. Add handlers to src/test/mocks/handlers.ts:

http.get('/api/endpoint', () => {
  return HttpResponse.json({ data: 'mock data' }, { status: 200 });
}),

Module Mocking

vi.mock('@/src/services/apiClient', () => ({
  apiClient: {
    get: vi.fn(),
    post: vi.fn(),
  },
}));

Next.js Features

Pre-mocked in vitest.setup.ts: - next/navigation (useRouter, usePathname, useSearchParams, useParams) - next/image - window.matchMedia - localStorage & sessionStorage

Coverage Thresholds

Current targets (in vitest.config.ts): - Lines: 60% - Functions: 60% - Branches: 60% - Statements: 60%

To check coverage:

bun run test:coverage

Coverage reports are generated in coverage/ directory.

Test File Organization

  • Place test files next to the source file with .test.ts(x) or .spec.ts(x) suffix
  • Group related tests using describe() blocks
  • Use meaningful test names that describe behavior
  • Keep tests focused and isolated

Common Test Patterns

Testing Async Operations

it('should handle async operations', async () => {
  const { result } = renderHook(() => useAsyncHook());

  await waitFor(() => {
    expect(result.current.isLoading).toBe(false);
  });

  expect(result.current.data).toBeDefined();
});

Testing State Changes

it('should update state', () => {
  const { result } = renderHook(() => useState(false));

  act(() => {
    result.current[1](true);
  });

  expect(result.current[0]).toBe(true);
});

Testing Error Handling

it('should handle errors', async () => {
  (apiClient.get as any).mockRejectedValue(new Error('Network error'));

  const result = await myFunction();

  expect(result).toBeNull(); // or expect specific error handling
});

Debugging Tests

Run with UI for visual debugging:

bun run test:ui

Use console.log() during tests - output will show in the test output.

For more complex debugging, check individual test file outputs.

Best Practices

  1. Isolation: Each test should be independent and not rely on others
  2. Clarity: Use descriptive test names (what, when, then pattern)
  3. Focus: Test one behavior per test
  4. Mocking: Mock external dependencies properly
  5. Coverage: Aim for good coverage but prioritize meaningful tests
  6. Maintainability: Keep tests simple and easy to understand

Next Steps

  • [ ] Add tests for remaining hooks
  • [ ] Add tests for form components
  • [ ] Add tests for page components
  • [ ] Set up CI/CD integration
  • [ ] Increase coverage to 80%+
  • [ ] Add E2E tests with Playwright or Cypress