Skip to content

Testing Guide

Backend Tests

Stack: Jest + @suites/unit TestBed automocks

Test files live alongside source files as *.spec.ts. E2E tests are in backend/test/.

# All unit tests
docker compose run --rm backend pnpm run test

# E2E tests
docker compose run --rm backend pnpm run test:e2e

# Single test file
docker compose run --rm backend pnpm exec jest <test-pattern>

# With coverage
docker compose run --rm backend pnpm run test -- --coverage

Backend Test Patterns

@suites/unit auto-mocks all dependencies injected into the service under test:

import { TestBed } from '@suites/unit';

describe('MessagesService', () => {
  let service: MessagesService;
  let prisma: Mocked<DatabaseService>;

  beforeAll(async () => {
    const { unit, unitRef } = await TestBed.create(MessagesService).compile();
    service = unit;
    prisma = unitRef.get(DatabaseService);
  });

  it('should create a message', async () => {
    prisma.message.create.mockResolvedValue(mockMessage);
    const result = await service.create(dto);
    expect(result).toEqual(mockMessage);
  });
});

Sensitive User Fields

When creating DTOs that include user data, test with expectNoSensitiveUserFields():

import { expectNoSensitiveUserFields } from '@/test-utils';

it('should not expose sensitive fields', () => {
  expectNoSensitiveUserFields(response.user);
});

Frontend Tests

Stack: Vitest + jsdom + Testing Library + MSW v2

Test files live in frontend/src/__tests__/ organized by type: components/, hooks/, features/.

# All tests
docker compose run --rm frontend pnpm run test

# With coverage
docker compose run --rm frontend pnpm run test:cov

Test Infrastructure

Located in frontend/src/__tests__/test-utils/:

renderWithProviders() wraps components with QueryClient, MemoryRouter, ThemeProvider, SocketContext, and NotificationProvider:

import { renderWithProviders } from '../test-utils';

it('renders channel list', async () => {
  const { user, queryClient } = renderWithProviders(<ChannelList />);
  // `user` is a userEvent instance for realistic interactions
  await user.click(screen.getByText('general'));
});

factories.ts provides factory functions:

import { createMessage, createChannel, createUser, createDmGroup } from '../test-utils/factories';

const message = createMessage({ content: 'hello' });
const channel = createChannel({ name: 'general', type: 'TEXT' });

msw/handlers.ts provides default MSW request handlers for auth, user profile, channels, and DMs. Override per-test with server.use(...).

Frontend Test Patterns

Mock API Client

vi.mock('../../api-client/client.gen', async (importOriginal) => {
  const { createClient, createConfig } = await importOriginal();
  return {
    ...await importOriginal(),
    client: createClient(createConfig({ baseUrl: 'http://localhost:3000' })),
  };
});

This lets MSW intercept requests from the generated client.

Mock Hooks

vi.mock('../../hooks/useFoo', () => ({
  useFoo: vi.fn(() => mockValue),
}));

Warning

vi.clearAllMocks() does not reset mockReturnValue. If any test overrides a mock, reset it explicitly in beforeEach.

Mock React Router

vi.mock('react-router-dom', async (importOriginal) => ({
  ...await importOriginal(),
  useParams: vi.fn(() => ({ channelId: '123' })),
  useNavigate: vi.fn(() => mockNavigate),
}));

Test Async Error Flows

it('shows error on failure', async () => {
  mockMutationFn.mockRejectedValue(new Error('Network error'));
  const { user } = renderWithProviders(<MyComponent />);

  await user.click(screen.getByRole('button', { name: 'Submit' }));

  expect(await screen.findByText('Network error')).toBeInTheDocument();
});

What to Test

  • Components: Rendering, user interactions, conditional display
  • Hooks: State changes, side effects, return values
  • Action functions: API calls, error handling, cache updates
  • Mock external dependencies to isolate the unit under test