# Mock Implementation Examples
## Overview
This document provides complete, working examples of mock implementations for each domain in the lokalise-mcp project. These examples follow the patterns established in the official Lokalise SDK tests.
## Complete Mock Setup
### 1. Vitest Configuration
```typescript
// vitest.config.ts
import { resolve } from "node:path";
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
setupFiles: ["./src/test-utils/setup.ts"],
include: ["src/**/*.test.ts"],
unstubGlobals: true,
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: [
"node_modules/",
"dist/",
"**/*.test.ts",
"src/test-utils/",
"scripts/",
],
},
clearMocks: true,
restoreMocks: true,
mockReset: true,
snapshotFormat: {
escapeString: false,
printBasicPrototype: false,
},
},
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
},
});
```
### 2. Test Setup File
```typescript
// src/test-utils/setup.ts
import { vi } from "vitest";
// Set test environment
process.env.NODE_ENV = "test";
process.env.LOKALISE_API_KEY = "test_api_key";
// Global test timeout
vi.setConfig({ testTimeout: 10000 });
// Mock console methods to reduce noise
global.console = {
...console,
log: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
};
// Add custom matchers
expect.extend({
toBeValidUUID(received: string) {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const pass = uuidRegex.test(received);
return {
pass,
message: () =>
pass
? `Expected ${received} not to be a valid UUID`
: `Expected ${received} to be a valid UUID`
};
}
});
```
## Projects Domain Mock Implementation
### Complete Projects Service Test
```typescript
// src/domains/projects/projects.service.test.ts
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { ProjectsService } from "./projects.service";
import { LokaliseApi } from "@lokalise/node-api";
import type { Project, PaginatedResult } from "@lokalise/node-api";
import { McpError } from "../../shared/utils/error.util";
// Mock the entire @lokalise/node-api module
vi.mock("@lokalise/node-api");
describe("ProjectsService", () => {
let service: ProjectsService;
let mockApi: vi.Mocked<LokaliseApi>;
let mockProjects: vi.Mocked<any>;
beforeEach(() => {
// Create mock projects methods
mockProjects = {
list: vi.fn(),
get: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
empty: vi.fn()
};
// Create mock API instance
mockApi = {
projects: vi.fn(() => mockProjects)
} as unknown;
// Mock the constructor
(LokaliseApi as vi.MockedClass<typeof LokaliseApi>).mockImplementation(
() => mockApi
);
// Create service instance
service = new ProjectsService();
});
afterEach(() => {
vi.clearAllMocks();
});
describe("listProjects", () => {
it("should list projects with default pagination", async () => {
// Arrange
const mockResponse: PaginatedResult<Project> = {
items: [
{
project_id: "proj_1",
name: "Project 1",
description: "First project",
created_at: "2024-01-01 00:00:00 (Etc/UTC)",
created_at_timestamp: 1704067200,
created_by: 12345,
created_by_email: "user@example.com",
team_id: 100,
base_language_id: 640,
base_language_iso: "en",
project_type: "localization_files",
settings: {
per_platform_key_names: false,
reviewing: true,
auto_toggle_unverified: true,
offline_translation: true,
key_editing: true,
inline_machine_translations: true,
branching: true,
segmentation: false,
contributor_preview_download_enabled: false,
custom_translation_statuses: false,
custom_translation_statuses_allow_multiple: false
},
statistics: {
progress_total: 75,
keys_total: 100,
team: 5,
base_words: 1000,
qa_issues_total: 10,
qa_issues: {
not_reviewed: 5,
unverified: 3,
spelling_grammar: 2,
inconsistent_placeholders: 0,
inconsistent_html: 0,
different_number_of_urls: 0,
different_urls: 0,
leading_whitespace: 0,
trailing_whitespace: 0,
different_number_of_email_address: 0,
different_email_address: 0,
different_brackets: 0,
different_numbers: 0,
double_space: 0,
special_placeholder: 0,
unbalanced_brackets: 0
},
languages: []
}
}
],
totalResults: 1,
totalPages: 1,
resultsPerPage: 100,
currentPage: 1,
hasNextPage: () => false,
hasPrevPage: () => false,
nextPage: () => 2,
prevPage: () => 0
};
mockProjects.list.mockResolvedValue(mockResponse);
// Act
const result = await service.listProjects({});
// Assert
expect(result).toEqual(mockResponse);
expect(mockProjects.list).toHaveBeenCalledWith({
page: 1,
limit: 100
});
});
it("should handle custom pagination parameters", async () => {
// Arrange
const mockResponse: PaginatedResult<Project> = {
items: [],
totalResults: 50,
totalPages: 5,
resultsPerPage: 10,
currentPage: 2,
hasNextPage: () => true,
hasPrevPage: () => true,
nextPage: () => 3,
prevPage: () => 1
};
mockProjects.list.mockResolvedValue(mockResponse);
// Act
const result = await service.listProjects({
page: 2,
limit: 10,
include_statistics: 1
});
// Assert
expect(result.currentPage).toBe(2);
expect(result.resultsPerPage).toBe(10);
expect(mockProjects.list).toHaveBeenCalledWith({
page: 2,
limit: 10,
include_statistics: 1
});
});
it("should handle API errors", async () => {
// Arrange
const apiError = new Error("API Error");
(apiError as unknown).response = {
status: 404,
data: { error: { message: "Projects not found" } }
};
mockProjects.list.mockRejectedValue(apiError);
// Act & Assert
await expect(service.listProjects({}))
.rejects
.toThrow(McpError);
try {
await service.listProjects({});
} catch (error) {
expect((error as McpError).code).toBe("LOKALISE_API_ERROR");
expect((error as McpError).message).toContain("Projects not found");
}
});
it("should handle rate limiting", async () => {
// Arrange
const rateLimitError = new Error("Too Many Requests");
(rateLimitError as unknown).response = {
status: 429,
data: { error: { message: "Rate limit exceeded" } },
headers: {
"x-rate-limit-limit": "6000",
"x-rate-limit-remaining": "0",
"x-rate-limit-reset": String(Date.now() + 3600000)
}
};
mockProjects.list.mockRejectedValue(rateLimitError);
// Act & Assert
await expect(service.listProjects({}))
.rejects
.toThrow("Rate limit exceeded");
});
});
describe("createProject", () => {
it("should create a new project", async () => {
// Arrange
const newProject = {
name: "New Project",
description: "A new test project",
base_language_iso: "en"
};
const createdProject: Project = {
project_id: "new_proj_123",
...newProject,
created_at: "2024-01-15 10:00:00 (Etc/UTC)",
created_at_timestamp: 1705314000,
created_by: 12345,
created_by_email: "creator@example.com",
team_id: 100,
base_language_id: 640,
project_type: "localization_files",
settings: {} as unknown,
statistics: {} as unknown
};
mockProjects.create.mockResolvedValue(createdProject);
// Act
const result = await service.createProject(newProject);
// Assert
expect(result).toEqual(createdProject);
expect(result.project_id).toBe("new_proj_123");
expect(mockProjects.create).toHaveBeenCalledWith(newProject);
});
it("should handle validation errors", async () => {
// Arrange
const invalidProject = { name: "" }; // Missing required fields
const validationError = new Error("Validation Error");
(validationError as unknown).response = {
status: 400,
data: {
error: {
message: "Validation failed",
errors: [
{ field: "name", message: "Name cannot be empty" },
{ field: "base_language_iso", message: "Base language is required" }
]
}
}
};
mockProjects.create.mockRejectedValue(validationError);
// Act & Assert
await expect(service.createProject(invalidProject))
.rejects
.toThrow(McpError);
});
});
describe("bulk operations", () => {
it("should handle bulk project operations", async () => {
// Arrange
const projectIds = ["proj_1", "proj_2", "proj_3"];
const deletePromises = projectIds.map(id =>
Promise.resolve({ project_deleted: true })
);
mockProjects.delete
.mockResolvedValueOnce(deletePromises[0])
.mockResolvedValueOnce(deletePromises[1])
.mockResolvedValueOnce(deletePromises[2]);
// Act
const results = await Promise.all(
projectIds.map(id => service.deleteProject(id))
);
// Assert
expect(results).toHaveLength(3);
expect(results.every(r => r.project_deleted)).toBe(true);
expect(mockProjects.delete).toHaveBeenCalledTimes(3);
});
});
});
```
## Keys Domain Mock Implementation with Cursor Pagination
```typescript
// src/domains/keys/keys.service.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { KeysService } from "./keys.service";
import { LokaliseApi } from "@lokalise/node-api";
import type { Key, PaginatedResult } from "@lokalise/node-api";
vi.mock("@lokalise/node-api");
describe("KeysService", () => {
let service: KeysService;
let mockApi: vi.Mocked<LokaliseApi>;
let mockKeys: vi.Mocked<any>;
beforeEach(() => {
mockKeys = {
list: vi.fn(),
get: vi.fn(),
create: vi.fn(),
update: vi.fn(),
bulk_update: vi.fn(),
delete: vi.fn(),
bulk_delete: vi.fn()
};
mockApi = {
keys: vi.fn(() => mockKeys)
} as unknown;
(LokaliseApi as vi.MockedClass<typeof LokaliseApi>).mockImplementation(
() => mockApi
);
service = new KeysService();
});
describe("listKeys with cursor pagination", () => {
it("should handle cursor pagination", async () => {
// Arrange
const mockResponse: PaginatedResult<Key> & {
nextCursor: string | null;
hasNextCursor: () => boolean;
responseTooBig: boolean;
} = {
items: [
{
key_id: 15519786,
created_at: "2024-01-20 14:22:33 (Etc/UTC)",
created_at_timestamp: 1705761753,
key_name: {
ios: "app_title",
android: "app_title",
web: "APP_TITLE",
other: "app.title"
},
filenames: {
ios: "Localizable.strings",
android: "strings.xml",
web: "en.json",
other: "app.yml"
},
description: "Application title",
platforms: ["ios", "android", "web"],
tags: ["ui", "important"],
comments: [],
screenshots: [],
translations: [],
is_plural: false,
plural_name: "",
is_hidden: false,
is_archived: false,
context: "",
base_words: 2,
char_limit: 50,
custom_attributes: {},
modified_at: "2024-01-20 14:22:33 (Etc/UTC)",
modified_at_timestamp: 1705761753
}
],
totalResults: 0, // Not provided in cursor pagination
totalPages: 0,
resultsPerPage: 100,
currentPage: 0,
nextCursor: "eyIxIjo0NDU5NjA2MX0=",
hasNextCursor: () => true,
responseTooBig: false,
hasNextPage: () => false,
hasPrevPage: () => false,
nextPage: () => 0,
prevPage: () => 0
};
mockKeys.list.mockResolvedValue(mockResponse);
// Act
const result = await service.listKeys({
project_id: "test_project",
pagination: "cursor",
limit: 100
});
// Assert
expect(result.nextCursor).toBe("eyIxIjo0NDU5NjA2MX0=");
expect(result.hasNextCursor()).toBe(true);
expect(result.responseTooBig).toBe(false);
expect(mockKeys.list).toHaveBeenCalledWith({
project_id: "test_project",
pagination: "cursor",
limit: 100
});
});
it("should handle next cursor pagination", async () => {
// Arrange - First page
const firstPageResponse = {
items: Array(100).fill(null).map((_, i) => ({
key_id: i + 1,
key_name: { web: `key_${i + 1}` }
})),
nextCursor: "eyIxIjo0NDU5NjA2MX0=",
hasNextCursor: () => true,
responseTooBig: false
};
// Arrange - Second page
const secondPageResponse = {
items: Array(50).fill(null).map((_, i) => ({
key_id: i + 101,
key_name: { web: `key_${i + 101}` }
})),
nextCursor: null,
hasNextCursor: () => false,
responseTooBig: false
};
mockKeys.list
.mockResolvedValueOnce(firstPageResponse as unknown)
.mockResolvedValueOnce(secondPageResponse as unknown);
// Act - First request
const firstResult = await service.listKeys({
project_id: "test_project",
pagination: "cursor",
limit: 100
});
// Act - Second request with cursor
const secondResult = await service.listKeys({
project_id: "test_project",
pagination: "cursor",
limit: 100,
cursor: firstResult.nextCursor
});
// Assert
expect(firstResult.items).toHaveLength(100);
expect(firstResult.hasNextCursor()).toBe(true);
expect(secondResult.items).toHaveLength(50);
expect(secondResult.hasNextCursor()).toBe(false);
});
it("should handle response too big header", async () => {
// Arrange
const mockResponse = {
items: [],
nextCursor: "eyIxIjo0NDU5NjA2MX0=",
hasNextCursor: () => true,
responseTooBig: true // Response was truncated
};
mockKeys.list.mockResolvedValue(mockResponse as unknown);
// Act
const result = await service.listKeys({
project_id: "test_project",
pagination: "cursor"
});
// Assert
expect(result.responseTooBig).toBe(true);
// Should handle by reducing limit or filtering
});
});
describe("bulk operations", () => {
it("should handle bulk key creation with partial failures", async () => {
// Arrange
const keysToCreate = [
{
key_name: { web: "new.key.1" },
translations: { en: "Translation 1" }
},
{
key_name: { web: "new.key.2" },
translations: { en: "Translation 2" }
},
{
key_name: { web: "duplicate.key" },
translations: { en: "Duplicate" }
}
];
const mockResponse = {
items: [
{ key_id: 1001, key_name: { web: "new.key.1" } },
{ key_id: 1002, key_name: { web: "new.key.2" } }
],
errors: [
{
key_name: { web: "duplicate.key" },
error: { message: "Key already exists" }
}
]
};
mockKeys.create.mockResolvedValue(mockResponse as unknown);
// Act
const result = await service.createKeys(
"test_project",
keysToCreate
);
// Assert
expect(result.items).toHaveLength(2);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].error.message).toBe("Key already exists");
});
it("should handle bulk key deletion", async () => {
// Arrange
const keyIds = [1001, 1002, 1003, 1004, 1005];
const mockResponse = {
keys_removed: true,
keys_locked: 0
};
mockKeys.bulk_delete.mockResolvedValue(mockResponse);
// Act
const result = await service.bulkDeleteKeys(
"test_project",
keyIds
);
// Assert
expect(result.keys_removed).toBe(true);
expect(mockKeys.bulk_delete).toHaveBeenCalledWith(
keyIds,
{ project_id: "test_project" }
);
});
it("should handle bulk update with validation", async () => {
// Arrange
const updates = [
{ key_id: 1001, description: "Updated description 1" },
{ key_id: 1002, description: "Updated description 2" }
];
const mockResponse = {
items: updates.map(u => ({
...u,
key_name: { web: `key_${u.key_id}` }
})),
errors: []
};
mockKeys.bulk_update.mockResolvedValue(mockResponse as unknown);
// Act
const result = await service.bulkUpdateKeys(
"test_project",
updates
);
// Assert
expect(result.items).toHaveLength(2);
expect(result.errors).toHaveLength(0);
});
});
});
```
## Error Handling Mock Examples
```typescript
// src/test-utils/error-mocks.test.ts
import { describe, it, expect, vi } from "vitest";
import { LokaliseApi } from "@lokalise/node-api";
import { McpError } from "../shared/utils/error.util";
vi.mock("@lokalise/node-api");
describe("Error Handling Examples", () => {
let mockApi: vi.Mocked<LokaliseApi>;
beforeEach(() => {
mockApi = new LokaliseApi({ apiKey: "test" }) as vi.Mocked<LokaliseApi>;
});
it("should handle 401 Unauthorized", async () => {
// Arrange
const error = new Error("Unauthorized");
(error as unknown).response = {
status: 401,
data: { error: { message: "Invalid API token", code: 401 } }
};
const mockProjects = {
list: vi.fn().mockRejectedValue(error)
};
mockApi.projects = vi.fn(() => mockProjects) as unknown;
// Act & Assert
await expect(mockApi.projects().list())
.rejects
.toThrow("Unauthorized");
try {
await mockApi.projects().list();
} catch (err) {
const mcpError = McpError.fromError(err);
expect(mcpError.code).toBe("AUTHENTICATION_ERROR");
expect(mcpError.details.status).toBe(401);
}
});
it("should handle 403 Forbidden", async () => {
// Arrange
const error = new Error("Forbidden");
(error as unknown).response = {
status: 403,
data: {
error: {
message: "You don't have permission to perform this action",
code: 403
}
}
};
const mockProjects = {
delete: vi.fn().mockRejectedValue(error)
};
mockApi.projects = vi.fn(() => mockProjects) as unknown;
// Act & Assert
await expect(mockApi.projects().delete("protected_project"))
.rejects
.toThrow("Forbidden");
});
it("should handle 404 Not Found", async () => {
// Arrange
const error = new Error("Not Found");
(error as unknown).response = {
status: 404,
data: { error: { message: "Project not found", code: 404 } }
};
const mockProjects = {
get: vi.fn().mockRejectedValue(error)
};
mockApi.projects = vi.fn(() => mockProjects) as unknown;
// Act & Assert
await expect(mockApi.projects().get("non_existent"))
.rejects
.toThrow("Not Found");
});
it("should handle 429 Rate Limiting with retry", async () => {
// Arrange
const rateLimitError = new Error("Too Many Requests");
(rateLimitError as unknown).response = {
status: 429,
data: { error: { message: "Rate limit exceeded", code: 429 } },
headers: {
"x-rate-limit-limit": "6000",
"x-rate-limit-remaining": "0",
"x-rate-limit-reset": String(Date.now() / 1000 + 60) // Reset in 60 seconds
}
};
const mockProjects = {
list: vi.fn()
.mockRejectedValueOnce(rateLimitError) // First call fails
.mockResolvedValueOnce({ items: [] }) // Retry succeeds
};
mockApi.projects = vi.fn(() => mockProjects) as unknown;
// Act - First attempt fails
await expect(mockApi.projects().list())
.rejects
.toThrow("Too Many Requests");
// Act - Retry after delay succeeds
const result = await mockApi.projects().list();
expect(result.items).toEqual([]);
expect(mockProjects.list).toHaveBeenCalledTimes(2);
});
it("should handle 500 Server Error", async () => {
// Arrange
const serverError = new Error("Internal Server Error");
(serverError as unknown).response = {
status: 500,
data: { error: { message: "An unexpected error occurred", code: 500 } }
};
const mockProjects = {
create: vi.fn().mockRejectedValue(serverError)
};
mockApi.projects = vi.fn(() => mockProjects) as unknown;
// Act & Assert
await expect(mockApi.projects().create({ name: "Test" }))
.rejects
.toThrow("Internal Server Error");
});
it("should handle validation errors with details", async () => {
// Arrange
const validationError = new Error("Bad Request");
(validationError as unknown).response = {
status: 400,
data: {
error: {
message: "Validation failed",
code: 400,
errors: [
{ field: "name", message: "Name is required" },
{ field: "base_language_iso", message: "Invalid language code" },
{ field: "description", message: "Description too long (max 1000 chars)" }
]
}
}
};
const mockProjects = {
create: vi.fn().mockRejectedValue(validationError)
};
mockApi.projects = vi.fn(() => mockProjects) as unknown;
// Act & Assert
try {
await mockApi.projects().create({});
} catch (err: unknown) {
expect(err.response.status).toBe(400);
expect(err.response.data.error.errors).toHaveLength(3);
expect(err.response.data.error.errors[0].field).toBe("name");
}
});
});
```
## Rate Limiting Mock Implementation
```typescript
// src/test-utils/rate-limiting.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
class RateLimitedApiMock {
private requestCount = 0;
private readonly limit = 10; // Low limit for testing
private resetTime = Date.now() + 60000;
async makeRequest<T>(
mockFn: vi.Mock,
successResponse: T
): Promise<T> {
this.requestCount++;
if (this.requestCount > this.limit) {
const error = new Error("Rate limit exceeded");
(error as unknown).response = {
status: 429,
headers: {
"x-rate-limit-limit": String(this.limit),
"x-rate-limit-remaining": "0",
"x-rate-limit-reset": String(this.resetTime / 1000)
}
};
throw error;
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 10));
return successResponse;
}
reset(): void {
this.requestCount = 0;
this.resetTime = Date.now() + 60000;
}
getRemaining(): number {
return Math.max(0, this.limit - this.requestCount);
}
}
describe("Rate Limiting Simulation", () => {
let rateLimiter: RateLimitedApiMock;
let mockApi: vi.Mock;
beforeEach(() => {
rateLimiter = new RateLimitedApiMock();
mockApi = vi.fn();
});
it("should allow requests within rate limit", async () => {
// Make 10 requests (within limit)
const promises = Array.from({ length: 10 }, (_, i) =>
rateLimiter.makeRequest(mockApi, { id: i })
);
const results = await Promise.all(promises);
expect(results).toHaveLength(10);
expect(rateLimiter.getRemaining()).toBe(0);
});
it("should reject requests exceeding rate limit", async () => {
// Make 10 requests to exhaust limit
await Promise.all(
Array.from({ length: 10 }, () =>
rateLimiter.makeRequest(mockApi, {})
)
);
// 11th request should fail
await expect(rateLimiter.makeRequest(mockApi, {}))
.rejects
.toThrow("Rate limit exceeded");
});
it("should reset after time window", async () => {
// Exhaust limit
await Promise.all(
Array.from({ length: 10 }, () =>
rateLimiter.makeRequest(mockApi, {})
)
);
// Reset manually (simulating time passage)
rateLimiter.reset();
// Should work again
const result = await rateLimiter.makeRequest(mockApi, { success: true });
expect(result).toEqual({ success: true });
});
});
```
## Bulk Operations Mock Example
```typescript
// src/domains/keys/keys.bulk.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { KeysService } from "./keys.service";
import { LokaliseApi } from "@lokalise/node-api";
vi.mock("@lokalise/node-api");
describe("Bulk Key Operations", () => {
let service: KeysService;
let mockKeys: vi.Mocked<any>;
beforeEach(() => {
mockKeys = {
create: vi.fn(),
bulk_update: vi.fn(),
bulk_delete: vi.fn()
};
const mockApi = {
keys: vi.fn(() => mockKeys)
} as unknown;
(LokaliseApi as vi.MockedClass<typeof LokaliseApi>).mockImplementation(
() => mockApi
);
service = new KeysService();
});
it("should create 1000 keys in batches", async () => {
// Arrange - Create 1000 keys
const keysToCreate = Array.from({ length: 1000 }, (_, i) => ({
key_name: { web: `key_${i + 1}` },
translations: {
en: `Translation ${i + 1}`,
fr: `Traduction ${i + 1}`
},
platforms: ["web"],
description: `Key number ${i + 1}`
}));
// Mock responses for 10 batches of 100 keys each
const batchResponses = Array.from({ length: 10 }, (_, batchIndex) => ({
items: keysToCreate
.slice(batchIndex * 100, (batchIndex + 1) * 100)
.map((key, i) => ({
key_id: batchIndex * 100 + i + 1,
...key
})),
errors: []
}));
batchResponses.forEach(response => {
mockKeys.create.mockResolvedValueOnce(response as unknown);
});
// Act - Process in batches
const results = [];
const batchSize = 100;
for (let i = 0; i < keysToCreate.length; i += batchSize) {
const batch = keysToCreate.slice(i, i + batchSize);
const result = await service.createKeys("test_project", batch);
results.push(result);
}
// Assert
expect(results).toHaveLength(10);
expect(mockKeys.create).toHaveBeenCalledTimes(10);
const totalCreated = results.reduce(
(sum, r) => sum + r.items.length,
0
);
expect(totalCreated).toBe(1000);
});
it("should handle partial failures in bulk operations", async () => {
// Arrange
const keysToUpdate = Array.from({ length: 100 }, (_, i) => ({
key_id: i + 1,
description: `Updated description ${i + 1}`
}));
// Simulate 90% success rate
const mockResponse = {
items: keysToUpdate.slice(0, 90).map(k => ({
...k,
key_name: { web: `key_${k.key_id}` }
})),
errors: keysToUpdate.slice(90).map(k => ({
key_id: k.key_id,
error: {
message: "Key is locked and cannot be updated",
code: "KEY_LOCKED"
}
}))
};
mockKeys.bulk_update.mockResolvedValue(mockResponse as unknown);
// Act
const result = await service.bulkUpdateKeys(
"test_project",
keysToUpdate
);
// Assert
expect(result.items).toHaveLength(90);
expect(result.errors).toHaveLength(10);
expect(result.errors[0].error.code).toBe("KEY_LOCKED");
});
it("should handle bulk deletion with locked keys", async () => {
// Arrange
const keyIds = Array.from({ length: 500 }, (_, i) => i + 1);
const mockResponse = {
keys_removed: true,
keys_locked: 25 // Some keys were locked and not deleted
};
mockKeys.bulk_delete.mockResolvedValue(mockResponse);
// Act
const result = await service.bulkDeleteKeys("test_project", keyIds);
// Assert
expect(result.keys_removed).toBe(true);
expect(result.keys_locked).toBe(25);
expect(mockKeys.bulk_delete).toHaveBeenCalledWith(
keyIds,
{ project_id: "test_project" }
);
});
});
```
## Performance Testing Mock
```typescript
// src/test-utils/performance.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { performance } from "perf_hooks";
describe("Performance Testing", () => {
it("should handle large datasets efficiently", async () => {
// Arrange - Create large dataset
const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`,
data: {
value: Math.random(),
timestamp: Date.now()
}
}));
const mockApi = {
process: vi.fn().mockImplementation(async (data) => {
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 1));
return { processed: data.length };
})
};
// Act - Measure performance
const startTime = performance.now();
// Process in chunks for better performance
const chunkSize = 1000;
const results = [];
for (let i = 0; i < largeDataset.length; i += chunkSize) {
const chunk = largeDataset.slice(i, i + chunkSize);
const result = await mockApi.process(chunk);
results.push(result);
}
const endTime = performance.now();
const duration = endTime - startTime;
// Assert
expect(results).toHaveLength(10);
expect(duration).toBeLessThan(1000); // Should complete in < 1 second
// Memory check
const memoryUsage = process.memoryUsage();
expect(memoryUsage.heapUsed).toBeLessThan(200 * 1024 * 1024); // < 200MB
});
it("should handle concurrent requests efficiently", async () => {
// Arrange
const mockApi = {
fetch: vi.fn().mockImplementation(async (id) => {
await new Promise(resolve => setTimeout(resolve, 10));
return { id, data: `Data for ${id}` };
})
};
// Act - Make 100 concurrent requests
const startTime = performance.now();
const promises = Array.from({ length: 100 }, (_, i) =>
mockApi.fetch(i + 1)
);
const results = await Promise.all(promises);
const endTime = performance.now();
const duration = endTime - startTime;
// Assert
expect(results).toHaveLength(100);
expect(duration).toBeLessThan(100); // Should parallelize well
expect(mockApi.fetch).toHaveBeenCalledTimes(100);
});
});
```
## Best Practices Summary
1. **Always mock at the module level** for unit tests
2. **Use realistic data structures** from API documentation
3. **Test error scenarios** comprehensively
4. **Include performance tests** for bulk operations
5. **Mock pagination headers** accurately
6. **Handle partial failures** in bulk operations
7. **Test rate limiting** behavior
8. **Verify memory usage** for large datasets
9. **Test concurrent operations** for scalability
10. **Keep mocks type-safe** with TypeScript
---
**Document Version**: 2.0.0
**Last Updated**: 2025-08-26
**Migration Status**: ✅ Fully migrated from Jest to Vitest
**Related**: API_MOCKING_GUIDE.md, TEST_FIXTURES_SPECIFICATION.md, DOMAIN_TEST_SPECIFICATIONS.md