Unit Testing with Vitest
Run and write unit tests with Vitest. Covers test execution, package-level testing, mocking patterns, and writing effective unit tests.
Unit tests use Vitest and live alongside packages in packages/**. They test business logic in isolation without browser or database dependencies.
Vitest runs fast, isolated tests that execute in milliseconds. Use unit tests for pure functions, validators, RBAC rules, and service logic. For testing complete user flows, use E2E tests with Playwright instead.
Running Tests
All Unit Tests
Run every unit test across the monorepo:
pnpm test:unitSingle Package
Test a specific package during development:
pnpm --filter @kit/rbac test:unitpnpm --filter @kit/organization-core test:unitpnpm --filter @kit/billing-ui test:unitWatch Mode
Automatically re-run tests when files change:
pnpm --filter @kit/rbac test:unit:watchCoverage Report
Generate a coverage report for a package:
pnpm --filter @kit/rbac test:unit:coverageTest Location
Tests live in __tests__ directories next to the code they test:
packages/rbac/├── src/│ ├── core/│ │ ├── factory.ts│ │ └── __tests__/│ │ └── factory.test.ts│ └── index.ts├── vitest.config.ts└── package.jsonWriting Tests
Basic Test Structure
import { describe, expect, it } from 'vitest';import { defineRBACConfig } from '../factory';describe('defineRBACConfig', () => { it('should return default resources when no config provided', () => { const config = defineRBACConfig({}); expect(config.resources).toEqual({ ORGANIZATION: 'organization', MEMBER: 'member', INVITATION: 'invitation', BILLING: 'billing', AC: 'ac', }); }); it('should merge custom resources with defaults', () => { const config = defineRBACConfig({ resources: { PROJECT: 'project', }, }); expect(config.resources.ORGANIZATION).toBe('organization'); expect(config.resources.PROJECT).toBe('project'); });});Testing Error Cases
import { describe, expect, it } from 'vitest';import { defineRBACConfig } from '../factory';import { RBACConfigError } from '../validator';describe('validation', () => { it('should throw error for duplicate resource values', () => { expect(() => defineRBACConfig({ resources: { PROJECT: 'organization', // Duplicate }, }), ).toThrow(RBACConfigError); });});Testing Async Code
import { describe, expect, it } from 'vitest';import { createInvitation } from '../invitations.service';describe('createInvitation', () => { it('should create invitation with valid email', async () => { const result = await createInvitation({ email: 'test@example.com', organizationId: 'org-123', role: 'member', }); expect(result.email).toBe('test@example.com'); expect(result.status).toBe('pending'); });});Mocking
Mock Dependencies
import { describe, expect, it, vi } from 'vitest';// Mock a modulevi.mock('../email-service', () => ({ sendEmail: vi.fn().mockResolvedValue({ success: true }),}));import { sendEmail } from '../email-service';import { createInvitation } from '../invitations.service';describe('createInvitation', () => { it('should send email after creating invitation', async () => { await createInvitation({ email: 'test@example.com' }); expect(sendEmail).toHaveBeenCalledWith( expect.objectContaining({ to: 'test@example.com', }) ); });});Mock Server-Only Imports
The shared Vitest config mocks server-only imports automatically:
export default defineConfig({ resolve: { alias: { 'server-only': new URL('./src/server-only-mock.ts', import.meta.url) .pathname, }, },});This allows testing server-side code without Next.js runtime errors.
Vitest Configuration
Each package uses a shared base config:
import vitestConfig from '@kit/vitest';export default vitestConfig;The shared config in tooling/vitest/vitest.config.ts:
import { defineConfig } from 'vitest/config';export default defineConfig({ test: { globals: true, environment: 'node', passWithNoTests: true, pool: 'forks', hookTimeout: 20_000, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], }, },});Packages with Tests
The kit includes unit tests for core business logic:
| Package | Tests |
|---|---|
@kit/rbac | Role-based access control, permissions, hierarchy |
@kit/organization-core | Memberships, invitations, seat enforcement, custom roles |
@kit/organization-hooks | Authorization hooks, invitation policies |
@kit/billing-ui | Billing actions, subscription logic, page loaders |
@kit/better-auth | Auth context, error handling, billing utilities |
@kit/policies | Policy evaluation |
@kit/admin | User admin service |
@kit/shared | Utility functions, mode detection |
@kit/ui | Route utilities |
@kit/database | Rate limiting service |
@kit/account-hooks | Account deletion service |
@kit/account-ui | MFA utilities |
@kit/cms/pagefind | Markdoc utilities |
Recommended Workflow
- While developing: Run tests for the package you're modifying in watch modepnpm --filter @kit/rbac test:unit:watch
- Before committing: Run all affected unit testspnpm test:unit
- For PRs: CI runs the full suite automatically
What to Unit Test
Good candidates:
- Pure functions (validators, transformers, formatters)
- Business logic (RBAC rules, seat enforcement, subscription calculations)
- Schema validations
- Error handling paths
- Edge cases in algorithms
Skip unit tests for:
- React components (use E2E instead)
- Database queries (use E2E with real DB)
- External API integrations (use E2E or mocked integration tests)
Test Commands Reference
# All unit tests (monorepo)pnpm test:unit# Single packagepnpm --filter @kit/rbac test:unit# Watch modepnpm --filter @kit/rbac test:unit:watch# Coveragepnpm --filter @kit/rbac test:unit:coverage# Run specific test filepnpm --filter @kit/rbac exec vitest run src/core/__tests__/factory.test.tsFrequently Asked Questions
How do I run tests in watch mode?
How do I mock a module in Vitest?
Why do I get 'server-only' import errors in tests?
Should I write unit tests for React components?
How do I generate a coverage report?
Where should I put my test files?
Next Steps
- E2E Testing with Playwright for testing complete user flows
- Writing Your Own Tests for patterns and best practices