Skip to main content
Glama
TEST_IMPLEMENTATION_GUIDE.md33.3 kB
# Test Implementation Guide ## Overview This guide provides detailed instructions for implementing tests across all layers of the lokalise-mcp project. Each domain follows a consistent structure with five test files covering different aspects of functionality. ## Test File Structure ### Domain Test Organization Each domain should have the following test structure: ``` src/domains/[domain]/ ├── [domain].service.test.ts # API interaction tests ├── [domain].controller.test.ts # Business logic tests ├── [domain].tool.test.ts # MCP tool tests ├── [domain].resource.test.ts # MCP resource tests ├── [domain].cli.test.ts # CLI command tests └── __fixtures__/ └── [domain].fixtures.ts # Test data and mocks ``` ## Layer-by-Layer Implementation ### 1. Service Layer Tests **Purpose**: Test direct API interactions with mocked Lokalise SDK **Coverage Target**: 95% **Focus**: API calls, error handling, data transformation #### Template Structure ```typescript // src/domains/projects/projects.service.test.ts import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { ProjectsService } from "./projects.service"; import { McpError } from "../../shared/utils/error.util"; import { createMockLokaliseApi } from "../../test-utils/mock-factory"; import { createProjectsListFixture, createProjectFixture, createErrorResponse } from "./__fixtures__/projects.fixtures"; describe("ProjectsService", () => { let service: ProjectsService; let mockApi: ReturnType<typeof createMockLokaliseApi>; beforeEach(() => { // Clear any module cache vi.clearAllMocks(); // Create mock API mockApi = createMockLokaliseApi(); // Create service instance service = new ProjectsService(); // Inject mock API (service as unknown).getLokaliseApi = () => mockApi; }); afterEach(() => { vi.restoreAllMocks(); }); describe("listProjects", () => { it("should fetch projects with default parameters", async () => { // Arrange const mockResponse = createProjectsListFixture(); mockApi.projects().list.mockResolvedValue(mockResponse); // Act const result = await service.listProjects({}); // Assert expect(result).toEqual(mockResponse); expect(mockApi.projects().list).toHaveBeenCalledWith({ page: 1, limit: 100 }); }); it("should handle pagination parameters", async () => { // Arrange const mockResponse = createProjectsListFixture({ page: 2, limit: 50 }); mockApi.projects().list.mockResolvedValue(mockResponse); // Act const result = await service.listProjects({ page: 2, limit: 50 }); // Assert expect(result.currentPage).toBe(2); expect(result.resultsPerPage).toBe(50); expect(mockApi.projects().list).toHaveBeenCalledWith({ page: 2, limit: 50 }); }); it("should handle API errors", async () => { // Arrange const error = createErrorResponse(404, "Projects not found"); mockApi.projects().list.mockRejectedValue(error); // Act & Assert await expect(service.listProjects({})) .rejects .toThrow(McpError); }); it("should handle rate limiting", async () => { // Arrange const rateLimitError = createErrorResponse(429, "Rate limit exceeded"); mockApi.projects().list.mockRejectedValue(rateLimitError); // Act & Assert await expect(service.listProjects({})) .rejects .toThrow("Rate limit exceeded"); }); }); describe("getProject", () => { it("should fetch a single project by ID", async () => { // Arrange const projectId = "test_project_123"; const mockProject = createProjectFixture({ project_id: projectId }); mockApi.projects().get.mockResolvedValue(mockProject); // Act const result = await service.getProject(projectId); // Assert expect(result).toEqual(mockProject); expect(mockApi.projects().get).toHaveBeenCalledWith(projectId); }); it("should handle non-existent project", async () => { // Arrange const error = createErrorResponse(404, "Project not found"); mockApi.projects().get.mockRejectedValue(error); // Act & Assert await expect(service.getProject("invalid_id")) .rejects .toThrow("Project not found"); }); }); describe("createProject", () => { it("should create a new project", async () => { // Arrange const newProject = { name: "New Project", description: "Test project" }; const mockResponse = createProjectFixture(newProject); mockApi.projects().create.mockResolvedValue(mockResponse); // Act const result = await service.createProject(newProject); // Assert expect(result.name).toBe("New Project"); expect(mockApi.projects().create).toHaveBeenCalledWith(newProject); }); it("should handle validation errors", async () => { // Arrange const error = createErrorResponse(400, "Invalid project name"); mockApi.projects().create.mockRejectedValue(error); // Act & Assert await expect(service.createProject({ name: "" })) .rejects .toThrow("Invalid project name"); }); }); describe("updateProject", () => { it("should update project settings", async () => { // Arrange const projectId = "test_project_123"; const updates = { name: "Updated Name" }; const mockResponse = createProjectFixture({ project_id: projectId, ...updates }); mockApi.projects().update.mockResolvedValue(mockResponse); // Act const result = await service.updateProject(projectId, updates); // Assert expect(result.name).toBe("Updated Name"); expect(mockApi.projects().update).toHaveBeenCalledWith( projectId, updates ); }); }); describe("deleteProject", () => { it("should delete a project", async () => { // Arrange const projectId = "test_project_123"; mockApi.projects().delete.mockResolvedValue({ project_deleted: true }); // Act const result = await service.deleteProject(projectId); // Assert expect(result.project_deleted).toBe(true); expect(mockApi.projects().delete).toHaveBeenCalledWith(projectId); }); it("should handle deletion of non-existent project", async () => { // Arrange const error = createErrorResponse(404, "Project not found"); mockApi.projects().delete.mockRejectedValue(error); // Act & Assert await expect(service.deleteProject("invalid_id")) .rejects .toThrow("Project not found"); }); }); describe("emptyProject", () => { it("should empty all keys and translations", async () => { // Arrange const projectId = "test_project_123"; mockApi.projects().empty.mockResolvedValue({ keys_deleted: true }); // Act const result = await service.emptyProject(projectId); // Assert expect(result.keys_deleted).toBe(true); expect(mockApi.projects().empty).toHaveBeenCalledWith(projectId); }); }); }); ``` ### 2. Controller Layer Tests **Purpose**: Test business logic, validation, and orchestration **Coverage Target**: 90% **Focus**: Input validation, error transformation, response formatting #### Template Structure ```typescript // src/domains/projects/projects.controller.test.ts import { describe, it, expect, beforeEach, vi } from "vitest"; import { projectsController } from "./projects.controller"; import * as projectsService from "./projects.service"; import { McpError } from "../../shared/utils/error.util"; import { createProjectsListFixture, createProjectFixture } from "./__fixtures__/projects.fixtures"; // Mock the service module vi.mock("./projects.service"); describe("ProjectsController", () => { const mockService = projectsService as vi.Mocked<typeof projectsService>; beforeEach(() => { vi.clearAllMocks(); }); describe("listProjects", () => { it("should validate input and call service", async () => { // Arrange const mockResponse = createProjectsListFixture(); mockService.listProjects.mockResolvedValue(mockResponse); // Act const result = await projectsController.listProjects({ page: 1, limit: 50, includeStats: true }); // Assert expect(result.content).toContain("# Projects List"); expect(result.data).toEqual(mockResponse); expect(mockService.listProjects).toHaveBeenCalledWith({ page: 1, limit: 50, include_statistics: 1 }); }); it("should apply default values", async () => { // Arrange mockService.listProjects.mockResolvedValue( createProjectsListFixture() ); // Act await projectsController.listProjects({}); // Assert expect(mockService.listProjects).toHaveBeenCalledWith({ page: 1, limit: 100, include_statistics: 1 }); }); it("should validate page number", async () => { // Act & Assert await expect(projectsController.listProjects({ page: -1 })) .rejects .toThrow("Page must be positive"); }); it("should validate limit range", async () => { // Act & Assert await expect(projectsController.listProjects({ limit: 1001 })) .rejects .toThrow("Limit must be between 1 and 1000"); }); it("should handle service errors gracefully", async () => { // Arrange mockService.listProjects.mockRejectedValue( new Error("API Error") ); // Act & Assert await expect(projectsController.listProjects({})) .rejects .toThrow(McpError); }); it("should format response correctly", async () => { // Arrange const projects = createProjectsListFixture({ projects: [ { project_id: "1", name: "Project A" }, { project_id: "2", name: "Project B" } ] }); mockService.listProjects.mockResolvedValue(projects); // Act const result = await projectsController.listProjects({}); // Assert expect(result.content).toContain("Project A"); expect(result.content).toContain("Project B"); expect(result.content).toContain("Total: 2 projects"); expect(result.metadata).toEqual({ total: 2, page: 1, hasMore: false }); }); }); describe("getProject", () => { it("should fetch and format single project", async () => { // Arrange const project = createProjectFixture({ project_id: "test_123", name: "Test Project", description: "A test project" }); mockService.getProject.mockResolvedValue(project); // Act const result = await projectsController.getProject("test_123"); // Assert expect(result.content).toContain("# Test Project"); expect(result.content).toContain("A test project"); expect(result.data).toEqual(project); }); it("should include languages if requested", async () => { // Arrange const project = createProjectFixture({ languages: [ { language_iso: "en", progress: 100 }, { language_iso: "fr", progress: 75 } ] }); mockService.getProject.mockResolvedValue(project); // Act const result = await projectsController.getProject("test_123", { includeLanguages: true }); // Assert expect(result.content).toContain("## Languages"); expect(result.content).toContain("English (en): 100%"); expect(result.content).toContain("French (fr): 75%"); }); }); describe("createProject", () => { it("should validate required fields", async () => { // Act & Assert await expect(projectsController.createProject({})) .rejects .toThrow("Name is required"); }); it("should create project with valid data", async () => { // Arrange const newProject = { name: "New Project", description: "Description", base_language_iso: "en" }; const created = createProjectFixture(newProject); mockService.createProject.mockResolvedValue(created); // Act const result = await projectsController.createProject(newProject); // Assert expect(result.content).toContain("Successfully created"); expect(result.content).toContain("New Project"); expect(mockService.createProject).toHaveBeenCalledWith(newProject); }); }); describe("updateProject", () => { it("should update project with partial data", async () => { // Arrange const updates = { name: "Updated Name" }; const updated = createProjectFixture(updates); mockService.updateProject.mockResolvedValue(updated); // Act const result = await projectsController.updateProject( "test_123", updates ); // Assert expect(result.content).toContain("Successfully updated"); expect(mockService.updateProject).toHaveBeenCalledWith( "test_123", updates ); }); }); describe("deleteProject", () => { it("should require confirmation", async () => { // Act & Assert await expect( projectsController.deleteProject("test_123", { confirm: false }) ).rejects.toThrow("Confirmation required"); }); it("should delete with confirmation", async () => { // Arrange mockService.deleteProject.mockResolvedValue({ project_deleted: true }); // Act const result = await projectsController.deleteProject("test_123", { confirm: true }); // Assert expect(result.content).toContain("Successfully deleted"); expect(mockService.deleteProject).toHaveBeenCalledWith("test_123"); }); }); }); ``` ### 3. Tool Layer Tests **Purpose**: Test MCP tool integration **Coverage Target**: 85% **Focus**: Schema validation, tool execution, error handling #### Template Structure ```typescript // src/domains/projects/projects.tool.test.ts import { describe, it, expect, beforeEach, vi } from "vitest"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { projectsTool } from "./projects.tool"; import { projectsController } from "./projects.controller"; import { z } from "zod"; vi.mock("./projects.controller"); describe("ProjectsTool", () => { let server: Server; const mockController = projectsController as vi.Mocked< typeof projectsController >; beforeEach(() => { server = new Server({ name: "test-server", version: "1.0.0" }); projectsTool.registerTools(server); vi.clearAllMocks(); }); describe("lokalise_list_projects", () => { it("should register tool with correct schema", () => { // Assert tool is registered const tools = server.getTools(); const listTool = tools.find(t => t.name === "lokalise_list_projects"); expect(listTool).toBeDefined(); expect(listTool?.description).toContain("List all projects"); expect(listTool?.inputSchema).toBeDefined(); }); it("should validate input parameters", async () => { // Arrange const invalidInput = { page: -1, limit: 5000 }; // Act & Assert await expect( server.callTool("lokalise_list_projects", invalidInput) ).rejects.toThrow("Validation error"); }); it("should execute with valid parameters", async () => { // Arrange mockController.listProjects.mockResolvedValue({ content: "# Projects\n- Project 1", data: { items: [] } }); // Act const result = await server.callTool("lokalise_list_projects", { page: 1, limit: 100, includeStats: true }); // Assert expect(result.content).toContain("Projects"); expect(mockController.listProjects).toHaveBeenCalledWith({ page: 1, limit: 100, includeStats: true }); }); it("should handle controller errors", async () => { // Arrange mockController.listProjects.mockRejectedValue( new Error("Controller error") ); // Act & Assert await expect( server.callTool("lokalise_list_projects", {}) ).rejects.toThrow("Controller error"); }); }); describe("lokalise_get_project", () => { it("should require project ID", async () => { // Act & Assert await expect( server.callTool("lokalise_get_project", {}) ).rejects.toThrow("Project ID is required"); }); it("should fetch project details", async () => { // Arrange mockController.getProject.mockResolvedValue({ content: "# Project Details", data: { project_id: "test_123" } }); // Act const result = await server.callTool("lokalise_get_project", { projectId: "test_123" }); // Assert expect(result.content).toContain("Project Details"); expect(mockController.getProject).toHaveBeenCalledWith("test_123"); }); }); describe("lokalise_create_project", () => { it("should validate required fields", async () => { // Act & Assert await expect( server.callTool("lokalise_create_project", { description: "No name provided" }) ).rejects.toThrow("Name is required"); }); it("should create project", async () => { // Arrange const projectData = { name: "New Project", description: "Test", base_language_iso: "en" }; mockController.createProject.mockResolvedValue({ content: "Created successfully", data: { ...projectData, project_id: "new_123" } }); // Act const result = await server.callTool( "lokalise_create_project", projectData ); // Assert expect(result.content).toContain("Created successfully"); expect(mockController.createProject).toHaveBeenCalledWith(projectData); }); }); describe("Tool Discovery", () => { it("should auto-register all domain tools", () => { const tools = server.getTools(); const projectTools = tools.filter(t => t.name.startsWith("lokalise_") && t.name.includes("project") ); expect(projectTools).toHaveLength(6); expect(projectTools.map(t => t.name)).toContain( "lokalise_list_projects", "lokalise_get_project", "lokalise_create_project", "lokalise_update_project", "lokalise_delete_project", "lokalise_empty_project" ); }); }); }); ``` ### 4. Resource Layer Tests **Purpose**: Test MCP resource handling **Coverage Target**: 85% **Focus**: URI parsing, query parameters, resource generation #### Template Structure ```typescript // src/domains/projects/projects.resource.test.ts import { describe, it, expect, beforeEach, vi } from "vitest"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { projectsResource } from "./projects.resource"; import { projectsController } from "./projects.controller"; vi.mock("./projects.controller"); describe("ProjectsResource", () => { let server: Server; const mockController = projectsController as vi.Mocked< typeof projectsController >; beforeEach(() => { server = new Server({ name: "test-server", version: "1.0.0" }); projectsResource.registerResources(server); vi.clearAllMocks(); }); describe("lokalise-projects", () => { it("should handle resource URI", async () => { // Arrange mockController.listProjects.mockResolvedValue({ content: "Projects list", data: { items: [] } }); // Act const result = await server.getResource( "lokalise://projects?page=1&limit=10" ); // Assert expect(result.content).toBe("Projects list"); expect(mockController.listProjects).toHaveBeenCalledWith({ page: 1, limit: 10 }); }); it("should parse query parameters", async () => { // Arrange const uri = "lokalise://projects?page=2&limit=50&includeStats=true"; mockController.listProjects.mockResolvedValue({ content: "Filtered projects", data: { items: [] } }); // Act await server.getResource(uri); // Assert expect(mockController.listProjects).toHaveBeenCalledWith({ page: 2, limit: 50, includeStats: true }); }); it("should use defaults for missing parameters", async () => { // Arrange mockController.listProjects.mockResolvedValue({ content: "Default projects", data: { items: [] } }); // Act await server.getResource("lokalise://projects"); // Assert expect(mockController.listProjects).toHaveBeenCalledWith({ page: 1, limit: 100 }); }); }); describe("lokalise-project-details", () => { it("should extract project ID from URI", async () => { // Arrange mockController.getProject.mockResolvedValue({ content: "Project details", data: { project_id: "test_123" } }); // Act const result = await server.getResource( "lokalise://projects/test_123" ); // Assert expect(result.content).toBe("Project details"); expect(mockController.getProject).toHaveBeenCalledWith("test_123"); }); it("should handle optional parameters", async () => { // Arrange const uri = "lokalise://projects/test_123?includeLanguages=true"; mockController.getProject.mockResolvedValue({ content: "Detailed project", data: {} }); // Act await server.getResource(uri); // Assert expect(mockController.getProject).toHaveBeenCalledWith("test_123", { includeLanguages: true }); }); it("should handle invalid URIs", async () => { // Act & Assert await expect( server.getResource("lokalise://invalid/uri") ).rejects.toThrow("Invalid resource URI"); }); }); }); ``` ### 5. CLI Layer Tests **Purpose**: Test CLI command integration **Coverage Target**: 80% **Focus**: Command registration, argument parsing, output formatting #### Template Structure ```typescript // src/domains/projects/projects.cli.test.ts import { describe, it, expect, beforeEach, vi } from "vitest"; import { Command } from "commander"; import { projectsCli } from "./projects.cli"; import { projectsController } from "./projects.controller"; vi.mock("./projects.controller"); describe("ProjectsCLI", () => { let program: Command; const mockController = projectsController as vi.Mocked< typeof projectsController >; beforeEach(() => { program = new Command(); program.exitOverride(); // Prevent process.exit in tests projectsCli.register(program); vi.clearAllMocks(); // Mock console methods vi.spyOn(console, "log").mockImplementation(); vi.spyOn(console, "error").mockImplementation(); }); afterEach(() => { vi.restoreAllMocks(); }); describe("list-projects command", () => { it("should register command with options", () => { const cmd = program.commands.find(c => c.name() === "list-projects"); expect(cmd).toBeDefined(); expect(cmd?.description()).toContain("List all projects"); expect(cmd?.options).toHaveLength(3); // page, limit, stats }); it("should execute with default options", async () => { // Arrange mockController.listProjects.mockResolvedValue({ content: "Projects list", data: { items: [] } }); // Act await program.parseAsync(["node", "test", "list-projects"]); // Assert expect(mockController.listProjects).toHaveBeenCalledWith({ page: 1, limit: 100, includeStats: false }); expect(console.log).toHaveBeenCalledWith("Projects list"); }); it("should parse command options", async () => { // Arrange mockController.listProjects.mockResolvedValue({ content: "Filtered list", data: { items: [] } }); // Act await program.parseAsync([ "node", "test", "list-projects", "--page", "2", "--limit", "50", "--stats" ]); // Assert expect(mockController.listProjects).toHaveBeenCalledWith({ page: 2, limit: 50, includeStats: true }); }); it("should handle errors gracefully", async () => { // Arrange mockController.listProjects.mockRejectedValue( new Error("API Error") ); // Act & Assert await expect( program.parseAsync(["node", "test", "list-projects"]) ).rejects.toThrow("API Error"); expect(console.error).toHaveBeenCalledWith( expect.stringContaining("Error") ); }); }); describe("get-project command", () => { it("should require project ID argument", async () => { // Act & Assert await expect( program.parseAsync(["node", "test", "get-project"]) ).rejects.toThrow("Missing required argument"); }); it("should fetch project details", async () => { // Arrange mockController.getProject.mockResolvedValue({ content: "Project details", data: {} }); // Act await program.parseAsync([ "node", "test", "get-project", "test_123" ]); // Assert expect(mockController.getProject).toHaveBeenCalledWith("test_123"); expect(console.log).toHaveBeenCalledWith("Project details"); }); }); describe("create-project command", () => { it("should prompt for required fields", async () => { // This would use inquirer or similar for interactive prompts // Mock the prompt responses const mockPrompt = vi.fn().mockResolvedValue({ name: "New Project", description: "Test project" }); (program as unknown).prompt = mockPrompt; mockController.createProject.mockResolvedValue({ content: "Created", data: {} }); // Act await program.parseAsync(["node", "test", "create-project"]); // Assert expect(mockPrompt).toHaveBeenCalled(); expect(mockController.createProject).toHaveBeenCalledWith({ name: "New Project", description: "Test project" }); }); it("should accept options directly", async () => { // Arrange mockController.createProject.mockResolvedValue({ content: "Created", data: {} }); // Act await program.parseAsync([ "node", "test", "create-project", "--name", "CLI Project", "--description", "From CLI" ]); // Assert expect(mockController.createProject).toHaveBeenCalledWith({ name: "CLI Project", description: "From CLI" }); }); }); describe("delete-project command", () => { it("should require confirmation", async () => { // Mock confirmation prompt const mockConfirm = vi.fn().mockResolvedValue(false); (program as unknown).confirm = mockConfirm; // Act await program.parseAsync([ "node", "test", "delete-project", "test_123" ]); // Assert expect(mockController.deleteProject).not.toHaveBeenCalled(); expect(console.log).toHaveBeenCalledWith("Deletion cancelled"); }); it("should delete with confirmation", async () => { // Mock confirmation prompt const mockConfirm = vi.fn().mockResolvedValue(true); (program as unknown).confirm = mockConfirm; mockController.deleteProject.mockResolvedValue({ content: "Deleted", data: {} }); // Act await program.parseAsync([ "node", "test", "delete-project", "test_123" ]); // Assert expect(mockController.deleteProject).toHaveBeenCalledWith( "test_123", { confirm: true } ); }); }); }); ``` ## Test Patterns and Best Practices ### 1. Mock Isolation Always isolate mocks to prevent test interference: ```typescript beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); }); afterEach(() => { vi.restoreAllMocks(); }); ``` ### 2. Test Data Builders Use builders for complex test data: ```typescript class ProjectBuilder { private project: Partial<Project> = {}; withId(id: string): this { this.project.project_id = id; return this; } withName(name: string): this { this.project.name = name; return this; } build(): Project { return { project_id: this.project.project_id || "default_id", name: this.project.name || "Default Project", // ... other defaults } as Project; } } ``` ### 3. Async Testing Always properly handle async operations: ```typescript // Good it("should handle async operations", async () => { const result = await asyncFunction(); expect(result).toBeDefined(); }); // Bad - may cause flaky tests it("should handle async operations", (done) => { asyncFunction().then(result => { expect(result).toBeDefined(); done(); }); }); ``` ### 4. Error Testing Test both error cases and success paths: ```typescript describe("error handling", () => { it.each([ [400, "Bad Request"], [401, "Unauthorized"], [403, "Forbidden"], [404, "Not Found"], [429, "Rate Limited"], [500, "Server Error"], ])("should handle %i error: %s", async (code, message) => { mockApi.method.mockRejectedValue( createErrorResponse(code, message) ); await expect(service.method()).rejects.toThrow(message); }); }); ``` ### 5. Coverage Verification Use coverage comments to track untested code: ```typescript /* istanbul ignore next - Defensive code, should not happen */ if (!apiKey) { throw new Error("API key missing"); } ``` ## Performance Testing ### Load Testing Template ```typescript describe("Performance", () => { it("should handle 1000 items efficiently", async () => { const largeDataset = Array.from({ length: 1000 }, (_, i) => createProjectFixture({ project_id: `proj_${i}` }) ); mockApi.projects().list.mockResolvedValue({ items: largeDataset, totalResults: 1000 }); const start = performance.now(); const result = await service.listProjects({ limit: 1000 }); const duration = performance.now() - start; expect(result.items).toHaveLength(1000); expect(duration).toBeLessThan(1000); // Should complete in < 1 second }); }); ``` ### Memory Testing ```typescript describe("Memory Usage", () => { it("should not leak memory in bulk operations", async () => { const initialMemory = process.memoryUsage().heapUsed; // Perform bulk operation for (let i = 0; i < 100; i++) { await service.bulkCreate(generateLargeDataset()); } // Force garbage collection (requires --expose-gc flag) if (global.gc) global.gc(); const finalMemory = process.memoryUsage().heapUsed; const memoryIncrease = finalMemory - initialMemory; // Memory increase should be minimal expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024); // < 50MB }); }); ``` ## Integration Testing ### End-to-End Test Template ```typescript describe("E2E: Project Workflow", () => { it("should complete full project lifecycle", async () => { // 1. Create project const created = await controller.createProject({ name: "E2E Test Project" }); expect(created.data.project_id).toBeDefined(); const projectId = created.data.project_id; // 2. Add languages await controller.addLanguages(projectId, ["en", "fr", "de"]); // 3. Add keys await controller.createKeys(projectId, [ { key_name: "app.title", translations: { en: "App Title" } } ]); // 4. Verify project state const project = await controller.getProject(projectId); expect(project.data.statistics.keys_total).toBe(1); expect(project.data.statistics.languages.length).toBe(3); // 5. Clean up await controller.deleteProject(projectId, { confirm: true }); }); }); ``` ## CI/CD Integration ### Jest Configuration for CI ```javascript // jest.ci.config.js module.exports = { ...baseConfig, coverageThreshold: { global: { branches: 90, functions: 90, lines: 90, statements: 90 } }, reporters: [ "default", ["vitest-junit", { outputDirectory: "test-results", outputName: "junit.xml" }] ], maxWorkers: 4, bail: true, // Stop on first test failure }; ``` --- **Document Version**: 1.0.0 **Last Updated**: 2025-08-24 **Related**: API_MOCKING_GUIDE.md, TEST_FIXTURES_SPECIFICATION.md, MOCK_IMPLEMENTATION_EXAMPLES.md

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