Skip to main content
Brainbox uses Vitest for testing across all packages and applications.

Running Tests

Basic Commands

npm run test              # Run all tests once
npm run test -- --watch   # Run tests in watch mode
npm run coverage          # Run tests with coverage report

Running Tests for Specific Packages

You can run tests for individual packages:
cd packages/core && npm run test
cd packages/client && npm run test
cd apps/server && npm run test
Use watch mode during development to automatically re-run tests as you make changes.

Writing Tests

Test File Location

Place test files:
  • Next to source files: user.tsuser.test.ts
  • In __tests__ directories: __tests__/user.test.ts
Both approaches are acceptable. Choose based on the package’s existing conventions.

Basic Test Structure

import { describe, it, expect } from 'vitest';
import { createNode } from './nodes';

describe('createNode', () => {
  it('should create a node with the given attributes', () => {
    const node = createNode({
      type: 'page',
      name: 'My Page',
    });

    expect(node.type).toBe('page');
    expect(node.name).toBe('My Page');
  });

  it('should generate a unique ID', () => {
    const node1 = createNode({ type: 'page', name: 'Page 1' });
    const node2 = createNode({ type: 'page', name: 'Page 2' });

    expect(node1.id).not.toBe(node2.id);
  });
});

Async Tests

Handle asynchronous operations with async/await:
import { describe, it, expect } from 'vitest';
import { queryNodes } from './queries';

describe('queryNodes', () => {
  it('should fetch nodes from the database', async () => {
    const nodes = await queryNodes({ type: 'page' });
    
    expect(nodes).toBeInstanceOf(Array);
    expect(nodes.length).toBeGreaterThan(0);
  });
});

Mocking

Use Vitest’s built-in mocking capabilities:
import { describe, it, expect, vi } from 'vitest';
import { syncEngine } from './sync-engine';
import { apiClient } from './api-client';

vi.mock('./api-client');

describe('syncEngine', () => {
  it('should call API when syncing', async () => {
    const mockPost = vi.fn().mockResolvedValue({ success: true });
    apiClient.post = mockPost;

    await syncEngine.sync();

    expect(mockPost).toHaveBeenCalledWith('/sync', expect.any(Object));
  });
});

Testing React Components

For React components, use Vitest with React Testing Library:
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter', () => {
  it('should increment count when button is clicked', async () => {
    const user = userEvent.setup();
    render(<Counter />);

    const button = screen.getByRole('button', { name: /increment/i });
    await user.click(button);

    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
});

Testing Best Practices

Descriptive Test Names

Use clear, descriptive test names that explain what is being tested: Bad:
it('works', () => {
  // ...
});
Good:
it('should return an error when email is invalid', () => {
  // ...
});

Test Behavior, Not Implementation

Focus on testing the public API and behavior, not internal implementation details: Bad:
it('should call validateEmail internally', () => {
  const spy = vi.spyOn(userService, 'validateEmail');
  userService.createUser({ email: 'test@example.com' });
  expect(spy).toHaveBeenCalled();
});
Good:
it('should reject invalid email addresses', () => {
  expect(() => {
    userService.createUser({ email: 'invalid-email' });
  }).toThrow('Invalid email');
});

Arrange, Act, Assert

Structure tests using the AAA pattern:
it('should calculate total price correctly', () => {
  // Arrange
  const cart = createCart();
  cart.addItem({ name: 'Book', price: 10 });
  cart.addItem({ name: 'Pen', price: 2 });

  // Act
  const total = cart.getTotal();

  // Assert
  expect(total).toBe(12);
});

Test Edge Cases

Don’t just test the happy path:
describe('divide', () => {
  it('should divide two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  it('should handle division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
  });

  it('should handle negative numbers', () => {
    expect(divide(-10, 2)).toBe(-5);
  });

  it('should handle decimal results', () => {
    expect(divide(10, 3)).toBeCloseTo(3.333, 2);
  });
});

Keep Tests Independent

Each test should be independent and not rely on the state from other tests:
import { describe, it, expect, beforeEach } from 'vitest';

describe('UserService', () => {
  let userService: UserService;

  beforeEach(() => {
    // Create fresh instance for each test
    userService = new UserService();
  });

  it('should create a user', () => {
    const user = userService.create({ name: 'Alice' });
    expect(user.name).toBe('Alice');
  });

  it('should delete a user', () => {
    const user = userService.create({ name: 'Bob' });
    userService.delete(user.id);
    expect(userService.find(user.id)).toBeUndefined();
  });
});

Testing Database Code

For code that interacts with the database, use an in-memory database or transaction rollback:
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { db } from './database';

describe('User queries', () => {
  beforeEach(async () => {
    await db.schema
      .createTable('users')
      .addColumn('id', 'text', (col) => col.primaryKey())
      .addColumn('name', 'text', (col) => col.notNull())
      .execute();
  });

  afterEach(async () => {
    await db.schema.dropTable('users').execute();
  });

  it('should insert and retrieve a user', async () => {
    await db.insertInto('users').values({
      id: '1',
      name: 'Alice',
    }).execute();

    const user = await db
      .selectFrom('users')
      .select(['id', 'name'])
      .where('id', '=', '1')
      .executeTakeFirst();

    expect(user?.name).toBe('Alice');
  });
});

Coverage Reports

Generate coverage reports to identify untested code:
npm run coverage
This generates a coverage report in the coverage/ directory.
Aim for high test coverage, but prioritize meaningful tests over coverage percentage.

Continuous Integration

Tests run automatically in CI for every pull request. All tests must pass before merging.

CI Test Command

npm run test  # Runs in CI mode (no watch)

Common Testing Patterns

Testing CRDT Operations

import { describe, it, expect } from 'vitest';
import * as Y from 'yjs';

describe('CRDT sync', () => {
  it('should merge concurrent updates', () => {
    const doc1 = new Y.Doc();
    const doc2 = new Y.Doc();

    const text1 = doc1.getText('content');
    const text2 = doc2.getText('content');

    text1.insert(0, 'Hello');
    text2.insert(0, 'World');

    // Apply updates
    Y.applyUpdate(doc1, Y.encodeStateAsUpdate(doc2));
    Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1));

    expect(text1.toString()).toBe(text2.toString());
  });
});

Testing WebSocket Sync

import { describe, it, expect, vi } from 'vitest';
import { SyncEngine } from './sync-engine';

describe('SyncEngine', () => {
  it('should reconnect on connection loss', async () => {
    const mockWebSocket = {
      send: vi.fn(),
      close: vi.fn(),
      addEventListener: vi.fn(),
    };

    const engine = new SyncEngine(mockWebSocket);
    
    // Simulate connection loss
    const closeHandler = mockWebSocket.addEventListener.mock.calls
      .find(([event]) => event === 'close')[1];
    closeHandler();

    // Verify reconnection attempt
    expect(engine.isReconnecting).toBe(true);
  });
});

Debugging Tests

Use the --reporter=verbose flag for detailed output:
npm run test -- --reporter=verbose
Run a single test file:
npm run test -- user.test.ts
Run tests matching a pattern:
npm run test -- --grep="should create user"

Build docs developers (and LLMs) love