This document describes the comprehensive automated testing setup for the Ottabase monorepo using Vitest, with support for all packages and applications.
The testing infrastructure includes:
- Vitest for fast, modern testing with ESM support
- @testing-library for component and DOM testing
- c8 for code coverage reporting
- Cloudflare Bindings Mocks for local testing of Worker code
- Root + Package-level configs for flexible, isolated testing
pnpm test # Run all tests in monorepo
pnpm test:all # Same as above, explicit
pnpm test:packages # Run only package tests
pnpm test:apps # Run only app testspnpm test:vite # Test Vite template app
pnpm test:next # Test Next.js template app
turbo test --filter=@ottabase/utils # Test specific packagepnpm test:coverage # Run tests with coverage (all)
pnpm test:coverage:packages # Coverage for packages only
pnpm test:coverage:apps # Coverage for apps onlypnpm test:watch # Watch mode for all tests
pnpm test:ui # Open Vitest UI for interactive testing- vitest.config.ts - Main Vitest config with workspace support
- vitest.setup.ts - Global setup for DOM APIs, mocks, etc.
Each package has its own vitest.config.ts:
packages/utils/- Node.js environmentpackages/api/- Node.js environmentpackages/auth/- Node.js environmentpackages/state/- Node.js environmentpackages/ui-components/- Browser/jsdom environmentpackages/ui-shadcn/- Browser/jsdom environmentpackages/forms/- Browser/jsdom environmentpackages/db/- Node.js environmentpackages/cf/- Node.js environment- And more...
apps/otta-web/vitest.config.ts- Vitest config with Cloudflare mocksapps/otta-web/vitest.setup.ts- Setup with all CF bindings mockedapps/otta-landing/vitest.config.ts- Next.js app Vitest configapps/otta-landing/vitest.setup.ts- Next.js mocks + CF bindings
Tests are placed in src/__tests__/ directory with .test.ts or .test.tsx extensions:
// packages/utils/src/__tests__/string.test.ts
import { describe, it, expect } from 'vitest';
import { isEmail, changeCase } from '../string';
describe('String Utilities', () => {
describe('isEmail', () => {
it('should validate correct emails', () => {
expect(isEmail('user@example.com')).toBe(true);
});
it('should reject invalid emails', () => {
expect(isEmail('invalid')).toBe(false);
});
});
});Use @testing-library/react for component testing:
// packages/ui-components/src/__tests__/Button.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Button } from '../Button';
describe('Button Component', () => {
it('should render button', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toBeTruthy();
});
});The apps have all Cloudflare bindings mocked via vitest.setup.ts:
// apps/otta-web/src/__tests__/database.test.ts
describe('Cloudflare D1 Integration', () => {
it('should have D1 mock available', () => {
expect((global as any).OBCF_D1).toBeDefined();
});
it('should mock D1 prepare method', async () => {
const db = (global as any).OBCF_D1;
const stmt = db.prepare('SELECT * FROM users');
expect(stmt).toBeDefined();
});
});it('should handle async operations', async () => {
const result = await fetchData();
expect(result).toBe('expected');
});
// With promises
it('should resolve promises', () => {
return expect(promise).resolves.toBe('value');
});All Cloudflare bindings are mocked in app test setups:
| Binding | Mock Methods | Purpose |
|---|---|---|
| OBCF_D1 | prepare, bind, all, first, run | SQLite database |
| OBCF_KV | get, put, delete, list | Key-value store |
| OBCF_R2 | get, put, delete, list | Object storage |
| OBCF_QUEUE | send, sendBatch | Message queue |
| OBCF_RATE_LIMITER | limit | Rate limiting |
| OBCF_REALTIME | get | Durable Objects |
| OBCF_ASSETS | fetch | Static asset serving |
You can customize mocks in your tests:
import { vi } from 'vitest';
(global as any).OBCF_D1.prepare = vi.fn().mockReturnValue({
all: vi.fn().mockResolvedValue([{ id: 1, name: 'test' }]),
});- Packages: 75% lines, 75% functions, 70% branches, 75% statements
- Apps: 70% lines, 70% functions, 65% branches, 70% statements
- Utils: 80% lines, 80% functions, 75% branches, 80% statements
- node_modules/, dist/, build/
- Config files (_.config.ts, _.config.js)
- Type definitions (*.d.ts)
- Index files (in some configs)
- Build outputs (.next/, .wrangler/, dist/)
# All coverage
pnpm test:coverage
# View HTML report
open coverage/index.html
# Specific format
turbo test -- --coverage.reporter=textEach package and app includes test scripts:
{
"scripts": {
"test": "vitest", // Run tests
"test:coverage": "vitest --coverage" // With coverage
}
}| Script | Purpose |
|---|---|
test |
Run all tests via Turbo |
test:all |
Explicit: test packages + apps |
test:packages |
Test all packages only |
test:apps |
Test all apps only |
test:vite |
Test Vite app |
test:next |
Test Next.js app |
test:coverage |
All tests with coverage |
test:coverage:packages |
Packages with coverage |
test:coverage:apps |
Apps with coverage |
test:watch |
Watch mode for all tests |
test:ui |
Interactive Vitest UI |
Test task in turbo.json:
- Depends On:
^build(packages must build first) - Inputs: Test files, configs, setup files
- Outputs: Coverage reports, vitest caches
- Environment: NODE_ENV=test
packages/utils/
├── src/
│ ├── __tests__/
│ │ ├── string.test.ts
│ │ ├── currency.test.ts
│ │ └── file.test.ts
│ ├── string.ts
│ ├── currency.ts
│ └── file.ts
└── vitest.config.ts
- Test files:
ComponentName.test.tsxormodule.test.ts - Test suites:
describe('Feature Name', ...) - Test cases:
it('should [expected behavior]', ...)
import { vi } from 'vitest';
vi.mock('../api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'John' }),
}));beforeEach(() => {
// Run before each test
});
afterEach(() => {
// Cleanup after each test
vi.clearAllMocks();
});- Tests run in parallel by default
- Use
describe.sequential()for tests that must run in order - Each test should be independent
// Promises
it('should resolve', async () => {
const result = await asyncFn();
expect(result).toBe('value');
});
// Callbacks
it('should call callback', (done) => {
asyncFn(() => {
expect(true).toBe(true);
done();
});
});
// Fake timers
it('should delay', async () => {
vi.useFakeTimers();
const promise = delayedFn();
vi.advanceTimersByTime(1000);
await promise;
vi.useRealTimers();
});Tests run in GitHub Actions:
- Trigger: On push and pull requests
- Platforms: Ubuntu, Windows, macOS
- Node Version: 24.x
- Status: Required for PR merge
Test failures are tracked but don't block CI (currently continue-on-error: true).
pnpm --filter @ottabase/utils test -- string.test.tspnpm --filter @ottabase/utils test -- --grep "isEmail"pnpm test:watchpnpm test:ui
# Opens http://localhost:51204/Add to .vscode/launch.json:
{
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "--inspect-brk"],
"console": "integratedTerminal"
}- Check
vitest.config.tsexists and is valid - Verify test files match pattern (_.test.ts, _.test.tsx)
- Ensure package.json has test script:
"test": "vitest"
- Check path aliases in
vitest.config.ts - Verify imports are correct
- Ensure dependencies are in package.json
- Confirm test files exercise the code
- Check coverage thresholds in config
- View HTML report for detailed coverage
- Verify vitest.setup.ts is referenced in vitest.config.ts
- Check
setupFilespath is correct - Ensure binding names match (OBCF_D1, OBCF_KV, etc.)
- Create
vitest.config.tsin package root - Create
src/__tests__/directory - Add test files with
.test.tsor.test.tsx - Add
testandtest:coveragescripts to package.json - Tests will be picked up by
pnpm test:packages
- Create
vitest.config.tsin app root - Create
vitest.setup.tswith necessary mocks - Create test files in
src/__tests__/or__tests__/ - Add
testandtest:coveragescripts to package.json - Tests will be picked up by
pnpm test:apps