Skip to main content

Testing Guide

This project uses Vitest for all testing (API and frontend).

Quick Start

# Run all tests
pnpm test

# Run API tests only
pnpm test:api

# Run web tests only
pnpm test:web

# Run tests in watch mode
pnpm test:watch

# Run tests with coverage
pnpm test:coverage

# Open Vitest UI
pnpm test:ui

Test Structure

API Tests (apps/api)

Location: src/**/*.spec.ts

Example:

import { describe, it, expect } from 'vitest';
import { MyService } from './my.service';

describe('MyService', () => {
it('should do something', () => {
expect(MyService).toBeDefined();
});
});

Web Tests (apps/web)

Location: src/**/*.test.{ts,tsx}

Example:

import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';

describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
});

Testing Stack

  • Test Runner: Vitest
  • React Testing: @testing-library/react
  • User Interactions: @testing-library/user-event
  • Assertions: @testing-library/jest-dom (matchers)

Best Practices

1. Test Organization

Group related tests using describe blocks:

describe('MyComponent', () => {
describe('when logged in', () => {
it('shows user menu', () => {});
});

describe('when logged out', () => {
it('shows login button', () => {});
});
});

2. Use Descriptive Test Names

// ❌ BAD
it('works', () => {});

// ✅ GOOD
it('should display error message when form is invalid', () => {});

3. Arrange, Act, Assert

it('should add two numbers', () => {
// Arrange
const a = 1;
const b = 2;

// Act
const result = add(a, b);

// Assert
expect(result).toBe(3);
});

4. Mock External Dependencies

import { vi } from 'vitest';

const mockFetch = vi.fn();
global.fetch = mockFetch;

it('fetches data', async () => {
mockFetch.mockResolvedValue({ json: () => ({ data: 'test' }) });
// Test implementation
});

5. Test User Behavior, Not Implementation

// ❌ BAD - testing implementation
expect(component.state.isOpen).toBe(true);

// ✅ GOOD - testing behavior
expect(screen.getByRole('dialog')).toBeVisible();

CI/CD Integration

Tests run automatically on:

  • Push to master, main, or develop branches
  • Pull requests to these branches

CI Pipeline

  • Unit tests – Fast job, no services required
  • E2E tests – Uses Postgres 16 and Redis 7 services; requires JWT_SECRET and OAuth env vars
  • Coverage – Runs after both pass; uploads to Codecov

See .github/workflows/test.yml for configuration.

Coverage

Coverage reports are generated in:

  • apps/api/coverage/
  • apps/web/coverage/

View HTML coverage reports:

# API
open apps/api/coverage/index.html

# Web
open apps/web/coverage/index.html

Common Testing Patterns

Testing React Components

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

it('handles user interaction', async () => {
const user = userEvent.setup();
render(<Button onClick={mockFn}>Click me</Button>);

await user.click(screen.getByRole('button'));
expect(mockFn).toHaveBeenCalled();
});

Testing Async Code

it('fetches data', async () => {
const promise = fetchData();
expect(promise).toBeInstanceOf(Promise);

const data = await promise;
expect(data).toEqual({ id: 1 });
});

Testing Errors

it('throws error for invalid input', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});

Troubleshooting

Tests Run Slowly

  • Use test.concurrent for independent tests
  • Mock expensive operations
  • Reduce test data size

Flaky Tests

  • Avoid hardcoded timeouts
  • Use waitFor for async operations
  • Mock date/time functions

Import Errors

  • Check vitest.config.ts for path aliases
  • Ensure dependencies are installed
  • Verify TypeScript configuration

Resources