Overview
The voting system uses Vitest with Cloudflare Workers testing support to run both unit and integration tests. Tests run in a simulated Workers environment with real D1 database access.
Test Structure
Directory Layout
tests/
├── helpers/
│ ├── fixtures.ts # Test data factories
│ ├── notion-mocks.ts # Notion API mocks
│ └── test-db.ts # Database test utilities
├── integration/
│ ├── auth.test.ts
│ ├── vote-flow.test.ts
│ ├── batch-invite-flow.test.ts
│ └── ... # End-to-end tests
├── unit/
│ ├── token.test.ts
│ ├── ballot.test.ts
│ └── ... # Unit tests
└── setup.ts # Test setup/teardown
Test Types
Unit Tests
Location: tests/unit/
Purpose: Test individual functions and modules in isolation.
Examples:
- Token generation and hashing
- Ballot validation
- Email template generation
- Notion parsing
Characteristics:
- Fast execution
- No external dependencies
- Mock external services
Integration Tests
Location: tests/integration/
Purpose: Test complete workflows end-to-end.
Examples:
- Full vote flow (create election → generate tokens → vote → view results)
- Batch invite flow
- Authentication flows
- Notion integration
Characteristics:
- Slower execution
- Real D1 database
- Real Workers environment
- Test actual HTTP requests
Running Tests
All Tests
npm testRuns all unit and integration tests.
Unit Tests Only
npm run test:unitRuns only tests in tests/unit/.
Integration Tests Only
npm run test:integrationRuns only tests in tests/integration/.
Watch Mode
npm test -- --watchRuns tests in watch mode, re-running on file changes.
Specific Test File
npm test tests/integration/vote-flow.test.tsRuns a specific test file.
Test Configuration
Vitest Config
Located in vitest.config.ts:
export default defineWorkersProject({
test: {
pool: '@cloudflare/vitest-pool-workers',
poolOptions: {
workers: {
main: './src/index.ts',
wrangler: { configPath: './wrangler.test.jsonc' },
},
},
include: ['tests/**/*.test.ts'],
testTimeout: 30000, // 30 second timeout
setupFiles: ['./tests/setup.ts'],
},
});Test Environment
Tests run in a simulated Cloudflare Workers environment:
- D1 Database: Real D1 database (test instance)
- Workers Runtime: Simulated Workers runtime
- Bindings: All bindings available (DB, TOKEN_MANAGER, etc.)
Test Database
Each test run:
- Creates a fresh D1 database
- Runs all migrations
- Cleans up after tests
See tests/setup.ts for database setup.
Test Helpers
Fixtures
Located in tests/helpers/fixtures.ts, provides factory functions:
createTestElection(): Creates a test election
const election = await createTestElection(env.DB, {
title: "Test Election",
ballot_type: "SIMPLE_TRIPLE",
open_at: new Date(now - 3600000).toISOString(),
close_at: new Date(now + 3600000).toISOString(),
});createTestToken(): Creates a test token
const { token, tokenHash } = await createTestToken(env.DB, election.id);createTestMagicLink(): Creates a test magic link
const magicLink = await createTestMagicLink(env.DB, "test@example.com");createTestInviteWithMode(): Creates a test invite
const invite = await createTestInviteWithMode(
env.DB,
election.id,
"voter@example.com",
"batch"
);Database Helpers
Located in tests/helpers/test-db.ts:
testNow: Provides a consistent “now” timestamp for tests
Notion Mocks
Located in tests/helpers/notion-mocks.ts:
Mocks Notion API responses for testing Notion integration without real API calls.
Writing Tests
Unit Test Example
import { describe, it, expect } from "vitest";
import { hashToken } from "../../src/models/token";
describe("Token Hashing", () => {
it("should hash tokens consistently", async () => {
const token = "test-token-123";
const hash1 = await hashToken(token);
const hash2 = await hashToken(token);
expect(hash1).toBe(hash2);
expect(hash1).not.toBe(token);
});
});Integration Test Example
import { describe, it, expect } from "vitest";
import { SELF, env } from "cloudflare:test";
import { createTestElection, createTestToken } from "../helpers/fixtures";
describe("Vote Flow", () => {
it("should allow voting with valid token", async () => {
const election = await createTestElection(env.DB, {
ballot_type: "SIMPLE_TRIPLE",
});
const { token } = await createTestToken(env.DB, election.id);
const response = await SELF.fetch(`http://localhost/e/${election.id}/vote`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, choice: "YES" }),
});
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
});
});Test Structure
- Arrange: Set up test data
- Act: Execute the code under test
- Assert: Verify the results
Best Practices
- Isolation: Each test should be independent
- Cleanup: Clean up test data after tests
- Descriptive Names: Use clear test descriptions
- One Assertion: Prefer one assertion per test (when possible)
- Fast Tests: Keep tests fast (< 1 second for unit, < 5 seconds for integration)
Mocking
External Services
Notion API: Mocked in tests/helpers/notion-mocks.ts
Email Service: Mocked via custom sender functions
Example:
const mockEmailSender = async (params) => {
sentEmails.push(params);
return { success: true };
};
await sendVoteInviteEmail(params, mockEmailSender);Durable Objects
Durable Objects are automatically available in test environment via env.TOKEN_MANAGER.
Test Coverage
Current Coverage
- Unit Tests: Core functions (tokens, ballots, validation)
- Integration Tests: Full workflows (voting, invites, results)
- Edge Cases: Error handling, invalid inputs
- Security: Token validation, authentication
Areas Needing More Tests
- Complex Condorcet scenarios
- High-concurrency scenarios
- Error recovery
- Performance under load
Continuous Integration
Running in CI
Tests should run in CI/CD pipeline:
# Example GitHub Actions
- name: Run tests
run: npm testTest Environment Variables
Tests use wrangler.test.jsonc for configuration:
- Test D1 database binding
- Test environment variables
- No production secrets
Debugging Tests
Running Single Test
npm test -- tests/integration/vote-flow.test.ts -t "should allow voting"Debug Output
Add console.log() statements (they appear in test output).
Test Timeout
Default timeout is 30 seconds. Increase if needed:
it("slow test", async () => {
// ...
}, { timeout: 60000 }); // 60 secondsCommon Issues
Database Not Found
Error: “Database not found”
Solution: Ensure wrangler.test.jsonc has correct D1 binding.
Timeout Errors
Error: “Test timeout”
Solution:
- Increase timeout
- Check for infinite loops
- Verify async operations complete
Flaky Tests
Causes:
- Race conditions
- Timing issues
- Shared state
Solutions:
- Use
awaitproperly - Isolate test data
- Use deterministic timestamps
Performance
Test Execution Time
- Unit Tests: < 1 second each
- Integration Tests: 1-5 seconds each
- Full Suite: ~30-60 seconds
Optimization
- Parallel Execution: Tests run in parallel when possible
- Fast Database: Test D1 is fast (in-memory)
- Minimal Setup: Only set up what’s needed
See Also
- ops - General infrastructure documentation
- voting-system - What’s being tested
- api-reference - API endpoints being tested