# 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.*