Skip to main content
Glama
NEW-DOMAIN-TESTING.md25.7 kB
# New Domain Testing Guide ## Quick Start for Domain Testing This guide provides a step-by-step approach to implementing comprehensive tests for new domains in the lokalise-mcp project. All patterns have been validated during Phase 1 Infrastructure Setup. **Phase 1 Status**: ✅ COMPLETE - Mock builder infrastructure fully operational with 113 passing tests! ## Understanding the Three-Tier Mocking Architecture Before implementing tests, understand our three distinct mocking approaches: ### Mock Type Selection Guide | Testing This Layer | Mock This Layer | Use This Mock Type | Example | |-------------------|-----------------|-------------------|---------| | Tool | Controller | Module Mock (`__mocks__`) | `vi.mock("./mydomain.controller.js")` | | Resource | Controller | Module Mock (`__mocks__`) | `vi.mock("./mydomain.controller.js")` | | CLI | Controller | Module Mock (`__mocks__`) | `vi.mock("./mydomain.controller.js")` | | Controller | Service | Module Mock + Mock Builder | `vi.mock("./mydomain.service.js")` + `MyDomainMockBuilder` | | Service | Lokalise API | Mock Factory | `createMockLokaliseApi()` | | Formatter | Nothing | Mock Builder for input | `MyDomainMockBuilder` | ## Testing Setup Checklist When creating tests for a new domain: ### 1. Create Test Infrastructure ```bash src/domains/mydomain/ ├── __mocks__/ │ └── mydomain.controller.js # Module mock for controller ├── __fixtures__/ │ └── mydomain.fixtures.ts # Static test data ├── __snapshots__/ │ └── mydomain.formatter.test.ts.snap # Auto-generated ├── mydomain.formatter.test.ts # Formatter tests ├── mydomain.controller.test.ts # Controller tests ├── mydomain.service.test.ts # Service tests ├── mydomain.tool.test.ts # Tool tests ├── mydomain.resource.test.ts # Resource tests └── mydomain.cli.test.ts # CLI tests ``` ### 2. Create Module Mock (For Tool/Resource/CLI Tests) **Template:** `src/domains/mydomain/__mocks__/mydomain.controller.js` ```javascript import { vi } from "vitest"; // Module mock replaces the entire controller for isolation testing export default { listItems: vi.fn(), getItemDetails: vi.fn(), createItem: vi.fn(), updateItem: vi.fn(), deleteItem: vi.fn(), // Add all controller methods your domain implements }; ``` **Usage in tool/resource/cli tests:** ```typescript // Automatically loads __mocks__/mydomain.controller.js vi.mock("./mydomain.controller.js"); import mydomainController from "./mydomain.controller.js"; const mockedController = vi.mocked(mydomainController); // In tests mockedController.listItems.mockResolvedValue({ content: "# Items List", data: [], metadata: {} }); ``` ### 3. Create Mock Builder (Phase 1 Pattern) **Template:** `src/test-utils/mock-builders/mydomain.mock.ts` ```typescript import type { MyDomainItem, PaginatedResult, BulkResult } from "@lokalise/node-api"; import { generators } from "../fixture-helpers/generators.js"; export class MyDomainMockBuilder { private items: MyDomainItem[] = []; private totalCount = 0; private page = 1; private limit = 100; private nextCursor: string | null = null; private useCursor = false; withItem(overrides: Partial<MyDomainItem> = {}): this { const timestamp = generators.timestamp(); const defaultItem: MyDomainItem = { // Define all required properties with defaults item_id: overrides.item_id ?? generators.id.myDomain(this.items.length), name: overrides.name ?? generators.myDomainName(this.items.length), created_at: overrides.created_at ?? timestamp.formatted, created_at_timestamp: overrides.created_at_timestamp ?? timestamp.timestamp, // ... add all other required properties }; this.items.push({ ...defaultItem, ...overrides }); this.totalCount++; return this; } // Add domain-specific builder methods withSpecialProperty(value: string): this { if (this.items.length === 0) { this.withItem(); } const lastItem = this.items[this.items.length - 1]; lastItem.specialProperty = value; return this; } withPagination(page: number, limit: number): this { this.page = page; this.limit = limit; this.useCursor = false; return this; } withCursorPagination(nextCursor: string | null, limit: number): this { this.nextCursor = nextCursor; this.limit = limit; this.useCursor = true; return this; } build(): PaginatedResult<MyDomainItem> & { nextCursor?: string | null; hasNextCursor?: () => boolean; } { if (this.useCursor) { return { items: this.items, totalResults: 0, totalPages: 0, resultsPerPage: this.limit, currentPage: 0, nextCursor: this.nextCursor, hasNextCursor: () => this.nextCursor !== null, hasNextPage: () => false, hasPrevPage: () => false, isFirstPage: () => true, isLastPage: () => this.nextCursor === null, nextPage: () => 0, prevPage: () => 0, }; } const totalPages = Math.ceil(this.totalCount / this.limit); return { items: this.items, totalResults: this.totalCount, totalPages, resultsPerPage: this.limit, currentPage: this.page, hasNextPage: () => this.page < totalPages, hasPrevPage: () => this.page > 1, isFirstPage: () => this.page === 1, isLastPage: () => this.page === totalPages, nextPage: () => (this.page < totalPages ? this.page + 1 : this.page), prevPage: () => (this.page > 1 ? this.page - 1 : this.page), nextCursor: null, hasNextCursor: () => false, }; } buildBulkResult(errors: Array<{ item: unknown; message: string }> = []): BulkResult<MyDomainItem> { return { items: this.items, errors: errors.map((e) => ({ ...(typeof e.item === "object" && e.item !== null ? e.item : {}), message: e.message, code: 400, })), } as BulkResult<MyDomainItem>; } } ``` ### 3. Create Static Fixtures **Template:** `src/domains/mydomain/__fixtures__/mydomain.fixtures.ts` ```typescript import type { MyDomainItem, BulkResult, PaginatedResult, CursorPaginatedResult, } from "@lokalise/node-api"; // Helper to create base item const createBaseItem = (overrides: Partial<MyDomainItem> = {}): MyDomainItem => ({ item_id: 0, name: "", created_at: "", created_at_timestamp: 0, // ... all required properties with defaults ...overrides, }); // List fixture - multiple items export const myDomainListFixture: MyDomainItem[] = [ createBaseItem({ item_id: 12345, name: "Test Item 1", created_at: "2023-01-01 12:00:00 (Etc/UTC)", created_at_timestamp: 1672574400, }), createBaseItem({ item_id: 12346, name: "Test Item 2", created_at: "2023-01-02 12:00:00 (Etc/UTC)", created_at_timestamp: 1672660800, }), ]; // Single item fixture export const myDomainItemFixture: MyDomainItem = createBaseItem({ item_id: 54321, name: "Detailed Test Item", created_at: "2023-01-15 10:30:00 (Etc/UTC)", created_at_timestamp: 1673776200, }); // Create operation fixture export const myDomainCreateFixture: BulkResult<MyDomainItem> = { items: [ createBaseItem({ item_id: 98765, name: "New Created Item", created_at: "2024-01-15 10:30:00 (Etc/UTC)", created_at_timestamp: 1705312200, }), ], errors: [], }; // Helper functions export const createMockPaginatedResult = <T>( items: T[], options: { totalResults?: number; totalPages?: number; currentPage?: number; resultsPerPage?: number; } = {}, ): PaginatedResult<T> => { const { totalResults = items.length, totalPages = 1, currentPage = 1, resultsPerPage = items.length, } = options; return { items, totalResults, totalPages, currentPage, resultsPerPage, hasNextPage: () => currentPage < totalPages, hasPrevPage: () => currentPage > 1, isFirstPage: () => currentPage <= 1, isLastPage: () => currentPage >= totalPages, nextPage: () => (currentPage < totalPages ? currentPage + 1 : currentPage), prevPage: () => (currentPage > 1 ? currentPage - 1 : currentPage), }; }; export const createMockCursorPaginatedResult = <T>( items: T[], options: { nextCursor?: string; prevCursor?: string; } = {}, ): CursorPaginatedResult<T> => { const { nextCursor, prevCursor } = options; return { items, nextCursor: nextCursor ?? null, hasNextCursor: () => !!nextCursor, hasPrevCursor: () => !!prevCursor, }; }; ``` ### 4. Create Formatter Tests (Phase 1 Validated Pattern) **Template:** `src/domains/mydomain/mydomain.formatter.test.ts` ```typescript import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import type { MyDomainItem, BulkResult } from "@lokalise/node-api"; import { generators } from "../../test-utils/fixture-helpers/generators.js"; import { MyDomainMockBuilder } from "../../test-utils/mock-builders/mydomain.mock.js"; // Load fixtures import { createMockCursorPaginatedResult, myDomainListFixture, myDomainItemFixture, myDomainCreateFixture, } from "./__fixtures__/mydomain.fixtures.js"; import { formatMyDomainList, formatMyDomainDetails, formatCreateMyDomainResult, } from "./mydomain.formatter.js"; describe("MyDomainFormatter", () => { // Phase 1 Validated Date Mocking Pattern const mockDate = new Date("2024-01-15T10:30:00.000Z"); let originalDate: DateConstructor; beforeAll(() => { originalDate = global.Date; global.Date = class extends originalDate { constructor(...args: ConstructorParameters<DateConstructor>) { if (args.length === 0) { super(mockDate.getTime()); } else { super(...args); } } static now() { return mockDate.getTime(); } } as DateConstructor; // CRITICAL: Preserve original static methods global.Date.UTC = originalDate.UTC; global.Date.parse = originalDate.parse; }); afterAll(() => { global.Date = originalDate; }); // Fresh mock builder per test (Phase 1 Pattern) let mockBuilder: MyDomainMockBuilder; beforeEach(() => { mockBuilder = new MyDomainMockBuilder(); }); const projectId = "803826145ba90b42d5d860.46800099"; describe("formatMyDomainList", () => { it("should format a list of items with fixtures", () => { const response = createMockCursorPaginatedResult(myDomainListFixture); const result = formatMyDomainList(response, projectId); expect(result).toMatchSnapshot(); }); it("should format items using mock builder", () => { const response = mockBuilder .withItem({ item_id: generators.id.myDomain(1), name: "Mock Builder Test Item", }) .withItem({ item_id: generators.id.myDomain(2), name: "Another Mock Item", }) .withPagination(1, 100) .build(); const result = formatMyDomainList(response, projectId); expect(result).toContain("2 items found"); expect(result).toContain("Mock Builder Test Item"); expect(result).toContain("Another Mock Item"); }); it("should handle empty list", () => { const response = mockBuilder.build(); // Empty by default const result = formatMyDomainList(response, projectId); expect(result).toMatchSnapshot(); }); it("should handle cursor pagination", () => { const response = mockBuilder .withItem({ name: "Cursor Test Item" }) .withCursorPagination("eyIxIjo1MjcyNjU2MTd9", 50) .build(); const result = formatMyDomainList(response, projectId); expect(result).toContain("Next cursor: eyIxIjo1MjcyNjU2MTd9"); expect(result).toMatchSnapshot(); }); it("should handle standard pagination", () => { const response = mockBuilder .withItem({ name: "Page 1 Item" }) .withItem({ name: "Page 2 Item" }) .withPagination(2, 1) // Page 2, 1 item per page .build(); const result = formatMyDomainList(response, projectId); expect(result).toContain("Page 2 of 2"); expect(result).toMatchSnapshot(); }); }); describe("formatMyDomainDetails", () => { it("should format detailed item information", () => { const result = formatMyDomainDetails(myDomainItemFixture, projectId); expect(result).toMatchSnapshot(); }); it("should handle item generated with mock builder", () => { const generatedItem = mockBuilder .withItem({ item_id: generators.id.myDomain(1), name: "Generated Test Item", // Add domain-specific properties }) .build().items[0]; const result = formatMyDomainDetails(generatedItem, projectId); expect(result).toContain("Generated Test Item"); expect(result).toContain("**Item ID**: " + generatedItem.item_id); }); }); describe("formatCreateMyDomainResult", () => { it("should format creation result", () => { const result = formatCreateMyDomainResult(myDomainCreateFixture, projectId); expect(result).toMatchSnapshot(); }); it("should handle creation errors using mock builder", () => { const bulkResult = mockBuilder .withItem({ name: "Successful Item" }) .buildBulkResult([ { item: { name: "Failed Item" }, message: "Validation error" } ]); const result = formatCreateMyDomainResult(bulkResult, projectId); expect(result).toContain("1 items created successfully"); expect(result).toContain("1 errors encountered"); expect(result).toContain("Validation error"); expect(result).toMatchSnapshot(); }); it("should handle bulk operations", () => { const bulkResult = mockBuilder .withItem({ name: "Success 1" }) .withItem({ name: "Success 2" }) .withItem({ name: "Success 3" }) .buildBulkResult([ { item: { name: "Error 1" }, message: "Name too long" }, { item: { name: "Error 2" }, message: "Invalid format" } ]); const result = formatCreateMyDomainResult(bulkResult, projectId); expect(result).toContain("3 items created successfully"); expect(result).toContain("2 errors encountered"); expect(result).toMatchSnapshot(); }); }); describe("Edge Cases", () => { it("should handle null and undefined values gracefully", () => { const itemWithNulls: MyDomainItem = { ...myDomainItemFixture, name: null as unknown, description: undefined as unknown, // Test with null/undefined values }; const result = formatMyDomainDetails(itemWithNulls, projectId); expect(result).toMatchSnapshot(); }); it("should handle special characters", () => { const itemWithSpecialChars: MyDomainItem = { ...myDomainItemFixture, name: "Item with <b>HTML</b> & \"quotes\" and 'apostrophes'", description: "Special chars: àáâãäå çñ üö ß", }; const result = formatMyDomainDetails(itemWithSpecialChars, projectId); expect(result).toMatchSnapshot(); }); it("should handle very long content", () => { const itemWithLongContent = mockBuilder .withItem({ name: "Item with very ".repeat(50) + "long name", description: "A".repeat(1000), }) .build().items[0]; const result = formatMyDomainDetails(itemWithLongContent, projectId); expect(result).toMatchSnapshot(); }); it("should handle empty arrays and objects", () => { const response = mockBuilder .withItem({ name: "Empty Data Item", tags: [], metadata: {}, }) .build(); const result = formatMyDomainList(response, projectId); expect(result).toMatchSnapshot(); }); }); describe("Mock Builder Validation", () => { it("should create items with all required properties", () => { const response = mockBuilder.withItem().build(); const item = response.items[0]; expect(item.item_id).toBeDefined(); expect(item.name).toBeDefined(); expect(item.created_at).toBeDefined(); expect(item.created_at_timestamp).toBeDefined(); }); it("should support method chaining", () => { const response = mockBuilder .withItem({ name: "First" }) .withItem({ name: "Second" }) .withPagination(1, 10) .build(); expect(response.items).toHaveLength(2); expect(response.currentPage).toBe(1); expect(response.resultsPerPage).toBe(10); }); it("should handle cursor and standard pagination separately", () => { const cursorResponse = mockBuilder .withItem({ name: "Cursor Item" }) .withCursorPagination("cursor-123", 50) .build(); const standardResponse = new MyDomainMockBuilder() .withItem({ name: "Standard Item" }) .withPagination(2, 25) .build(); expect(cursorResponse.nextCursor).toBe("cursor-123"); expect(cursorResponse.hasNextCursor()).toBe(true); expect(standardResponse.currentPage).toBe(2); expect(standardResponse.resultsPerPage).toBe(25); }); }); }); ``` ## Generator Extensions (Phase 1 Pattern) Add domain-specific generators to `src/test-utils/fixture-helpers/generators.ts`: ```typescript export const generators = { // ... existing generators // Add your domain generators myDomainName: (index: number) => { const names = ["Sample Item", "Test Domain", "Mock Entry"]; return names[index % names.length] + ` ${index}`; }, id: { // ... existing id generators myDomain: (index: number) => 20000000 + index, }, // Add domain-specific generator patterns myDomainStatus: (index: number) => { const statuses = ["new", "in_progress", "completed", "failed"]; return statuses[index % statuses.length]; }, }; ``` ## Testing Workflow (Phase 1 Validated) ### 1. Write Tests First (TDD Approach) ```bash # 1. Create test files with failing tests touch src/domains/mydomain/mydomain.formatter.test.ts # 2. Run tests (they will fail) npm test mydomain.formatter.test.ts # 3. Implement formatter to make tests pass # 4. Refactor and improve # 5. Generate snapshots npm test -- --updateSnapshot ``` ### 2. Validation Commands (Phase 1 Sequence) ```bash # Run domain-specific tests npm test mydomain # Check TypeScript compilation (must be clean) npm run build # Verify formatting and linting npm run format && npm run lint # Check test coverage npm run test:coverage # All commands must pass before marking complete ``` ### 3. Quality Gates (Phase 1 Standards) Before considering tests complete: - [ ] All tests pass: `npm test mydomain` - [ ] TypeScript compiles: `npm run build` (zero errors) - [ ] Linting passes: `npm run lint` - [ ] Formatting applied: `npm run format` - [ ] Mock builders used: No manual mock creation - [ ] Fresh instances: New builder per test (`beforeEach`) - [ ] Date mocking: Consistent timestamps in snapshots - [ ] Snapshots reviewed: Changes are intentional - [ ] Edge cases covered: null, undefined, special chars - [ ] No console errors or warnings ## Phase 1 Mock Builder Examples ### Real Examples from Phase 1 Testing #### Keys Domain Mock Builder Usage ```typescript // From keys.formatter.test.ts (passing tests) const mockBuilder = new KeysMockBuilder(); const response = mockBuilder .withKey({ key_id: 15519786, description: "Test key for snapshot validation" }) .withTranslations([ { language_iso: "en", translation: "Hello World" }, { language_iso: "fr", translation: "Bonjour le monde" } ]) .withCursorPagination("eyIxIjo1MjcyNjU2MTd9", 100) .build(); ``` #### Projects Domain Mock Builder Usage ```typescript // From projects.formatter.test.ts (passing tests) const mockBuilder = new ProjectsMockBuilder(); const response = mockBuilder .withProject({ name: "Mobile Localization Project", statistics: { keys_total: 150, languages_total: 5, progress_total: 75 } }) .withPagination(1, 20) .build(); ``` #### Tasks Domain Mock Builder Usage ```typescript // From tasks.formatter.test.ts (passing tests) const mockBuilder = new TasksMockBuilder(); const response = mockBuilder .withTask({ title: "Translate mobile interface", status: "in_progress", progress: { total: 100, completed: 60 } }) .withAssignees([ { user_id: 123, email: "translator@example.com" } ]) .build(); ``` ## Common Patterns by Domain Type ### For CRUD Domains (Projects, Keys, Tasks) Test these formatter functions: - `formatDomainList()` - List view with pagination ✅ - `formatDomainDetails()` - Single item details ✅ - `formatCreateDomainResult()` - Creation results ✅ - `formatUpdateDomainResult()` - Update results - `formatDeleteDomainResult()` - Deletion results - `formatBulkUpdateDomainResult()` - Bulk operations - `formatBulkDeleteDomainResult()` - Bulk deletion ### For Read-Only Domains (System Languages, Contributors) Test these formatter functions: - `formatDomainList()` - List view ✅ - `formatDomainDetails()` - Single item details ✅ - Handle pagination appropriately ✅ ### For Specialized Domains (Comments, Screenshots) - Focus on relationship formatting (parent-child) - Test rich content rendering (HTML, images) - Handle nested data structures ## Anti-Patterns to Avoid (Phase 1 Lessons) ### ❌ Don't Copy-Paste Tests ```typescript // ❌ Bad: Copied from another domain without adaptation it("should format key details", () => { // Test is about keys but domain is tasks }); ``` ### ❌ Don't Skip Edge Cases ```typescript // ❌ Bad: Only happy path testing describe("MyDomainFormatter", () => { it("should work", () => { // Only tests perfect data }); }); ``` ### ❌ Don't Use Manual Mocks ```typescript // ❌ Bad: Manual mock creation (TypeScript errors guaranteed) const mockResult = { items: [{ id: 1 /* missing 50+ properties */ }] }; ``` ### ❌ Don't Share Mock Builder Instances ```typescript // ❌ Bad: Shared state between tests const builder = new MockBuilder(); // Outside test describe("Tests", () => { it("test 1", () => { builder.withItem({ id: 1 }); // Affects other tests }); }); ``` ### ✅ Follow Phase 1 Validated Patterns ```typescript // ✅ Good: Use mock builders with fresh instances describe("MyDomainFormatter", () => { let mockBuilder: MyDomainMockBuilder; beforeEach(() => { mockBuilder = new MyDomainMockBuilder(); // Fresh instance }); // Date mocking setup (Phase 1 pattern) beforeAll(() => { // Validated date mocking setup }); describe("formatMyDomainList", () => { it("should format list with standard pagination", () => { const response = mockBuilder .withItem({ name: "Test Item" }) .withPagination(1, 10) .build(); const result = formatMyDomainList(response, projectId); expect(result).toMatchSnapshot(); }); it("should handle empty list", () => { const response = mockBuilder.build(); // Empty by default const result = formatMyDomainList(response, projectId); expect(result).toMatchSnapshot(); }); }); describe("Edge Cases", () => { it("should handle null values gracefully", () => { const response = mockBuilder .withItem({ name: null as unknown, description: undefined as unknown }) .build(); const result = formatMyDomainDetails(response.items[0], projectId); expect(result).toMatchSnapshot(); }); it("should handle special characters", () => { const response = mockBuilder .withItem({ name: "Item with <b>HTML</b> & \"quotes\"" }) .build(); const result = formatMyDomainDetails(response.items[0], projectId); expect(result).toMatchSnapshot(); }); }); }); ``` ## Next Steps After setting up domain tests: 1. **Run initial tests**: `npm test mydomain` (should pass) 2. **Generate snapshots**: `npm test -- --updateSnapshot` (review carefully) 3. **Review snapshots**: Ensure output looks correct and contains expected content 4. **Add controller tests**: If needed for business logic validation 5. **Add service tests**: If complex API integration exists 6. **Update coverage**: Ensure above current threshold (18.18% baseline) 7. **Quality validation**: All Phase 1 quality gates must pass ## Phase 1 Success Metrics ### Template Effectiveness ✅ - **113 tests passing** using these exact patterns - **0 test failures** across all domains - **66 snapshot tests** stable with date mocking - **4 mock builders** operational and validated - **Sub-second execution** (0.663s total) ### Pattern Validation ✅ All patterns in this guide have been tested and validated: - ✅ Mock builder creation and usage - ✅ Date mocking for consistent snapshots - ✅ Fresh instance management - ✅ Fluent API chaining - ✅ Pagination handling (standard + cursor) - ✅ Bulk operation testing - ✅ Edge case coverage - ✅ Type safety enforcement ### Quality Standards ✅ - ✅ TypeScript compilation: Zero errors - ✅ Linting: All checks passed - ✅ Formatting: Code properly formatted - ✅ Coverage: Baseline maintained (18.18%) - ✅ Performance: Fast execution - ✅ Reliability: No flaky tests --- *This guide reflects successful Phase 1 Infrastructure Setup. All patterns have been tested and validated with 113 passing tests. Use these exact patterns for reliable domain testing.*

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AbdallahAHO/lokalise-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server