--- name: encore-testing description: Write or run automated tests for Encore.ts code with `encore test` and vitest/jest. Covers isolated per-test databases, calling handlers directly, and `describe`/`it`/`expect`. when_to_use: >- User wants to add/write/fix a test, write a vitest or jest spec, test an endpoint or service, set up `encore test`, configure isolated test databases, write `beforeEach`/`afterEach` for db cleanup, mock external dependencies, or assert on API request/response behaviour. Trigger phrases: "write a test", "write a vitest test", "add tests for", "vitest", "jest", "encore test", "test the endpoint", "test the service", "integration test", "isolated database". --- # Testing Encore.ts Applications ## Instructions Encore.ts uses standard TypeScript testing tools. The recommended setup is Vitest. ### Setup Vitest ```bash npm install -D vitest ``` Add to `package.json`: ```json { "scripts": { "test": "vitest" } } ``` ### Test an API Endpoint ```typescript // api.test.ts import { describe, it, expect } from "vitest"; import { hello } from "./api"; describe("hello endpoint", () => { it("returns a greeting", async () => { const response = await hello(); expect(response.message).toBe("Hello, World!"); }); }); ``` ### Run Tests ```bash # Run with Encore (recommended - sets up infrastructure) encore test # Or run directly with npm npm test ``` Using `encore test` is recommended because it: - Sets up test databases automatically - Provides isolated infrastructure per test - Handles service dependencies ### Test with Request Parameters ```typescript // api.test.ts import { describe, it, expect } from "vitest"; import { getUser } from "./api"; describe("getUser endpoint", () => { it("returns the user by ID", async () => { const user = await getUser({ id: "123" }); expect(user.id).toBe("123"); expect(user.name).toBeDefined(); }); }); ``` ### Test Database Operations Encore provides isolated test databases: ```typescript // user.test.ts import { describe, it, expect, beforeEach } from "vitest"; import { createUser, getUser, db } from "./user"; describe("user operations", () => { beforeEach(async () => { // Clean up before each test await db.exec`DELETE FROM users`; }); it("creates and retrieves a user", async () => { const created = await createUser({ email: "test@example.com", name: "Test" }); const retrieved = await getUser({ id: created.id }); expect(retrieved.email).toBe("test@example.com"); }); }); ``` ### Test Service-to-Service Calls ```typescript // order.test.ts import { describe, it, expect } from "vitest"; import { createOrder } from "./order"; describe("order service", () => { it("creates an order and notifies user service", async () => { // Service calls work normally in tests const order = await createOrder({ userId: "user-123", items: [{ productId: "prod-1", quantity: 2 }], }); expect(order.id).toBeDefined(); expect(order.status).toBe("pending"); }); }); ``` ### Test Error Cases ```typescript import { describe, it, expect } from "vitest"; import { getUser } from "./api"; import { APIError } from "encore.dev/api"; describe("error handling", () => { it("throws NotFound for missing user", async () => { await expect(getUser({ id: "nonexistent" })) .rejects .toThrow("user not found"); }); it("throws with correct error code", async () => { try { await getUser({ id: "nonexistent" }); } catch (error) { expect(error).toBeInstanceOf(APIError); expect((error as APIError).code).toBe("not_found"); } }); }); ``` ### Test Pub/Sub ```typescript // notifications.test.ts import { describe, it, expect, vi } from "vitest"; import { orderCreated } from "./events"; describe("pub/sub", () => { it("publishes order created event", async () => { const messageId = await orderCreated.publish({ orderId: "order-123", userId: "user-456", total: 9999, }); expect(messageId).toBeDefined(); }); }); ``` ### Test Cron Jobs Test the underlying function, not the cron schedule: ```typescript // cleanup.test.ts import { describe, it, expect } from "vitest"; import { cleanupExpiredSessions } from "./cleanup"; describe("cleanup job", () => { it("removes expired sessions", async () => { // Create some expired sessions first await createExpiredSession(); // Call the endpoint directly await cleanupExpiredSessions(); // Verify cleanup happened const remaining = await countSessions(); expect(remaining).toBe(0); }); }); ``` ### Mocking External Services ```typescript import { describe, it, expect, vi, beforeEach } from "vitest"; import { sendWelcomeEmail } from "./email"; // Mock external API vi.mock("./external-email-client", () => ({ send: vi.fn().mockResolvedValue({ success: true }), })); describe("email service", () => { it("sends welcome email", async () => { const result = await sendWelcomeEmail({ userId: "123" }); expect(result.sent).toBe(true); }); }); ``` ### Test Configuration Create `vite.config.ts` (required for `~encore` imports): ```typescript /// import { defineConfig } from "vite"; import path from "path"; export default defineConfig({ resolve: { alias: { "~encore": path.resolve(__dirname, "./encore.gen"), }, }, test: { globals: true, environment: "node", include: ["**/*.test.ts"], coverage: { reporter: ["text", "json", "html"], }, }, }); ``` ### VS Code Integration Install the [Vitest extension](https://marketplace.visualstudio.com/items?itemName=vitest.explorer) and add to `.vscode/settings.json`: ```json { "vitest.commandLine": "encore test" } ``` **Note:** For VS Code test explorer, disable file-level parallelism to avoid port conflicts: ```typescript // vite.config.ts export default defineConfig({ // ... test: { fileParallelism: false, // Disable for VS Code // ... }, }); ``` Re-enable for CI: `encore test --fileParallelism=true` ### Guidelines - Use `encore test` to run tests with infrastructure setup - Each test file gets an isolated database transaction (rolled back after) - Test API endpoints by calling them directly as functions - Service-to-service calls work normally in tests - Mock external dependencies (third-party APIs, email services, etc.) - Don't mock Encore infrastructure (databases, Pub/Sub) - use the real thing