UNPKG

oneie

Version:

Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.

567 lines (447 loc) 15 kB
--- title: Technical Test Template dimension: knowledge category: patterns tags: backend, frontend, testing related_dimensions: groups, people, things scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the knowledge dimension in the patterns category. Location: one/knowledge/patterns/test/technical-test-template.md Purpose: Documents pattern: technical test template Related dimensions: groups, people, things For AI agents: Read this to understand technical test template. --- # Pattern: Technical Test Template **Category:** Testing **Context:** When defining unit, integration, and E2E tests that validate implementation **Problem:** Need comprehensive test coverage that validates code works correctly at all levels ## Solution Write unit tests for services, integration tests for Convex functions, E2E tests for full user flows. Use existing test frameworks (Vitest, Playwright). ## Template ```markdown # Technical Tests: {Feature Name} **Feature:** {N}-{M}-{feature-name} **User Flows Reference:** {N}-{M}-{feature-name}/tests.md **Acceptance Criteria Reference:** {N}-{M}-{feature-name}/tests.md (AC section) **Date:** YYYY-MM-DD --- ## Test Strategy **Framework:** - Unit Tests: Vitest - Integration Tests: Vitest + Convex Test Client - E2E Tests: Playwright **Test Location:** - Unit: `backend/convex/services/__tests__/` - Integration: `backend/convex/__tests__/` - E2E: `frontend/test/e2e/` **Coverage Target:** > 80% for services, > 70% overall --- ## Unit Tests (Services) ### Service: {EntityName}Service **File:** `backend/convex/services/__tests__/{EntityName}Service.test.ts` #### Test: create() with valid data ```typescript describe("{EntityName}Service", () => { describe("create", () => { it("should create {entity} with valid data", async () => { // Arrange const mockDb = createMockDb(); const service = {EntityName}ServiceLive; const input: Create{EntityName}Input = { name: "Test {Entity}", properties: { description: "Test description" }, groupId: "group_123" as Id<"groups">, }; // Act const result = await Effect.runPromise( service.create(input).pipe( Effect.provideService(DatabaseService, mockDb) ) ); // Assert expect(result).toBeDefined(); expect(mockDb.insert).toHaveBeenCalledWith("things", { type: "{entityType}", name: "Test {Entity}", properties: { description: "Test description" }, groupId: "group_123", status: "draft", createdAt: expect.any(Number), updatedAt: expect.any(Number), }); }); it("should fail with ValidationError for empty name", async () => { // Arrange const service = {EntityName}ServiceLive; const input: Create{EntityName}Input = { name: "", properties: {}, groupId: "group_123" as Id<"groups">, }; // Act & Assert await expect( Effect.runPromise(service.create(input)) ).rejects.toMatchObject({ _tag: "{EntityName}ValidationError", message: "Name is required", }); }); }); describe("getById", () => { it("should return {entity} when exists", async () => { // Arrange const mockDb = createMockDb(); const entityId = "thing_123" as Id<"things">; const mock{Entity} = { _id: entityId, name: "Test {Entity}", type: "{entityType}", properties: {}, groupId: "group_123" as Id<"groups">, status: "active", createdAt: Date.now(), updatedAt: Date.now(), }; mockDb.get.mockResolvedValue(mock{Entity}); const service = {EntityName}ServiceLive; // Act const result = await Effect.runPromise( service.getById(entityId).pipe( Effect.provideService(DatabaseService, mockDb) ) ); // Assert expect(result).toEqual(mock{Entity}); }); it("should fail with NotFound error when {entity} doesn't exist", async () => { // Arrange const mockDb = createMockDb(); mockDb.get.mockResolvedValue(null); const service = {EntityName}ServiceLive; const entityId = "thing_404" as Id<"things">; // Act & Assert await expect( Effect.runPromise(service.getById(entityId)) ).rejects.toMatchObject({ _tag: "{EntityName}NotFound", id: entityId, }); }); }); describe("update", () => { it("should update {entity} successfully", async () => { // Test update logic }); }); describe("delete", () => { it("should soft delete {entity}", async () => { // Test delete (status = archived) }); }); }); ``` **Acceptance Criteria Mapping:** AC-10 (Data stored correctly) --- ## Integration Tests (Convex Functions) ### Mutation: create{EntityName} **File:** `backend/convex/__tests__/mutations/{entities}.test.ts` #### Test: Create {entity} with authentication ```typescript describe("mutations.{entities}.create", () => { it("should create {entity} when authenticated", async () => { // Arrange const t = convexTest(schema); await t.run(async (ctx) => { // Set up auth ctx.auth = { getUserIdentity: async () => ({ tokenIdentifier: "user_123", email: "test@example.com", groupId: "group_123" as Id<"groups">, }), }; // Act const entityId = await ctx.mutation(api.mutations.things.create, { type: "{entityType}", name: "Test {Entity}", groupId: "group_123" as Id<"groups">, properties: { description: "Test" }, }); // Assert expect(entityId).toBeDefined(); const entity = await ctx.db.get(entityId); expect(entity).toMatchObject({ name: "Test {Entity}", type: "{entityType}", groupId: "group_123", status: "draft", }); }); }); it("should fail when not authenticated", async () => { // Arrange const t = convexTest(schema); await t.run(async (ctx) => { ctx.auth = { getUserIdentity: async () => null }; // Act & Assert await expect( ctx.mutation(api.mutations.things.create, { type: "{entityType}", name: "Test", groupId: "group_123" as Id<"groups">, properties: {}, }) ).rejects.toThrow("Unauthorized"); }); }); it("should fail with validation error for invalid data", async () => { // Test validation }); }); ``` **Acceptance Criteria Mapping:** AC-1 (User can create), AC-10 (Data storage) --- ### Query: list{Entities} **File:** `backend/convex/__tests__/queries/{entities}.test.ts` #### Test: List {entities} with multi-tenancy ```typescript describe("queries.things.list", () => { it("should only return {entities} from user's group", async () => { // Arrange const t = convexTest(schema); await t.run(async (ctx) => { // Create {entities} in different groups const group1Id = await ctx.db.insert("groups", { name: "Group 1", slug: "group-1", type: "organization" }); const group2Id = await ctx.db.insert("groups", { name: "Group 2", slug: "group-2", type: "organization" }); await ctx.db.insert("things", { name: "{Entity} 1", type: "{entityType}", groupId: group1Id, status: "active", properties: {}, createdAt: Date.now(), updatedAt: Date.now(), }); await ctx.db.insert("things", { name: "{Entity} 2", type: "{entityType}", groupId: group2Id, status: "active", properties: {}, createdAt: Date.now(), updatedAt: Date.now(), }); // Set auth to group1 ctx.auth = { getUserIdentity: async () => ({ tokenIdentifier: "user_123", groupId: group1Id, }), }; // Act const results = await ctx.query(api.queries.things.list, { type: "{entityType}", }); // Assert expect(results).toHaveLength(1); expect(results[0].name).toBe("{Entity} 1"); expect(results[0].groupId).toBe(group1Id); }); }); it("should return empty array when not authenticated", async () => { // Test unauthenticated access }); }); ``` **Acceptance Criteria Mapping:** AC-2 (Display list), AC-10 (Multi-tenancy) --- ## E2E Tests (Full User Flows) ### Flow: Create {Entity} **File:** `frontend/test/e2e/{entity}-create.spec.ts` #### Test: User can create {entity} successfully ```typescript import { test, expect } from "@playwright/test"; test.describe("{Entity} Creation Flow", () => { test("should create {entity} successfully", async ({ page }) => { // Arrange - Navigate to page await page.goto("/"); // Sign in await page.getByLabel("Email").fill("test@example.com"); await page.getByLabel("Password").fill("password123"); await page.getByRole("button", { name: "Sign In" }).click(); // Wait for redirect await expect(page).toHaveURL("/{entities}"); // Act - Click create button await page.getByRole("button", { name: "New {Entity}" }).click(); // Wait for modal await expect(page.getByRole("dialog")).toBeVisible(); // Fill form await page.getByLabel("Name").fill("Test {Entity}"); await page.getByLabel("Description").fill("This is a test {entity}"); await page.getByLabel("Status").selectOption("active"); // Submit await page.getByRole("button", { name: "Create {Entity}" }).click(); // Assert - Success feedback await expect(page.getByText("{Entity} created successfully")).toBeVisible(); // Assert - {Entity} appears in list await expect(page.getByText("Test {Entity}")).toBeVisible(); // Assert - Modal closes await expect(page.getByRole("dialog")).not.toBeVisible(); }); test("should show validation errors for invalid input", async ({ page }) => { // Arrange await page.goto("/{entities}"); await page.getByRole("button", { name: "New {Entity}" }).click(); // Act - Submit without filling required fields await page.getByRole("button", { name: "Create {Entity}" }).click(); // Assert - Validation errors shown await expect(page.getByText("Name is required")).toBeVisible(); }); test("should handle network errors gracefully", async ({ page }) => { // Arrange await page.goto("/{entities}"); // Simulate offline await page.context().setOffline(true); // Act - Try to create await page.getByRole("button", { name: "New {Entity}" }).click(); await page.getByLabel("Name").fill("Test"); await page.getByRole("button", { name: "Create {Entity}" }).click(); // Assert - Error message shown await expect(page.getByText(/network error|offline/i)).toBeVisible(); }); }); ``` **Acceptance Criteria Mapping:** AC-1 (Create flow), AC-3 (Validation), AC-7 (Error messages) **User Flow Mapping:** Flow 1 --- ## Performance Tests ### Load Test: List {Entities} ```typescript test("should load {entities} list in < 500ms", async ({ page }) => { // Arrange - Seed database with 100 {entities} // (done in beforeEach hook) // Act & Measure const start = Date.now(); await page.goto("/{entities}"); await page.waitForSelector("[data-testid='{entity}-card']"); const duration = Date.now() - start; // Assert expect(duration).toBeLessThan(500); }); ``` **Acceptance Criteria Mapping:** AC-5 (Performance) --- ## Test Execution **Run all tests:** ```bash # Unit tests cd backend && bun test # Integration tests cd backend && bun test:integration # E2E tests cd frontend && bun test:e2e # All tests bun test:all ``` **Coverage report:** ```bash bun test:coverage ``` --- ## Test Data Setup **Seed data for tests:** ```typescript // test/fixtures/{entity}-fixtures.ts export const valid{Entity}Data = { name: "Test {Entity}", properties: { description: "Test description", // Add entity-specific fields }, groupId: "group_test" as Id<"groups">, }; export const invalid{Entity}Data = { name: "", // Empty name (should fail) properties: {}, groupId: "group_test" as Id<"groups">, }; ``` --- ## Test Coverage Map | Acceptance Criteria | Unit Tests | Integration Tests | E2E Tests | |---------------------|------------|-------------------|-----------| | AC-1: Create {entity} | ✅ Service | ✅ Mutation | ✅ Full flow | | AC-2: Display list | ✅ Service | ✅ Query | ✅ Page load | | AC-3: Validation | ✅ Service | ✅ Mutation | ✅ Form errors | | AC-4: Real-time | - | ✅ Subscription | ✅ Multi-user | | AC-5: Performance | - | - | ✅ Load test | **Overall Coverage:** {N}% (target: > 80%) --- ## Continuous Testing **Pre-commit hook:** ```bash # .git/hooks/pre-commit bun test:unit ``` **CI/CD pipeline:** ```yaml # .github/workflows/test.yml - name: Unit Tests run: bun test:unit - name: Integration Tests run: bun test:integration - name: E2E Tests run: bun test:e2e ``` --- ## Test Maintenance **When to update tests:** - Acceptance criteria change - New edge cases discovered - Performance targets change - Implementation refactored **Test Review Checklist:** - [ ] Test name clearly describes what's tested - [ ] Arrange-Act-Assert pattern followed - [ ] Test is independent (no shared state) - [ ] Test data is realistic - [ ] Assertions are specific - [ ] Error cases tested - [ ] Performance measured where applicable ``` ## Variables - `{Feature Name}` - Human-readable feature name - `{N}-{M}-{feature-name}` - Feature ID - `{EntityName}` - PascalCase entity name - `{entity}` - Lowercase entity name - `{Entity}` - Title case entity name - `{entities}` - Plural lowercase - `{entityType}` - Entity type value in ontology ## Usage 1. Copy template to `one/things/features/{N}-{M}-{feature-name}/tests.md` (append to same file) 2. Write unit tests for all service methods 3. Write integration tests for all mutations/queries 4. Write E2E tests for each user flow 5. Map tests to acceptance criteria 6. Set up test data fixtures 7. Configure CI/CD to run tests ## Common Mistakes - **Mistake:** Testing implementation details instead of behavior - **Fix:** Test public API, not internal methods - **Mistake:** Tests depend on each other (shared state) - **Fix:** Each test should be independent, use fresh fixtures - **Mistake:** No test data fixtures - **Fix:** Create reusable test data objects - **Mistake:** Not testing error cases - **Fix:** Always test happy path AND error paths - **Mistake:** E2E tests too slow - **Fix:** Use mock API for E2E when possible, test integration separately ## Related Patterns - **user-flow-template.md** - What to test (user perspective) - **acceptance-criteria-template.md** - Success criteria (measurable) - **service-template.md** - What to unit test - **mutation-template.md** - What to integration test