231 lines
5.7 KiB
Markdown
231 lines
5.7 KiB
Markdown
# Handler Test Template
|
|
|
|
Test handlers directly without transport layer using Bun test runner.
|
|
|
|
## Template
|
|
|
|
```typescript
|
|
import { describe, test, expect, beforeEach } from "bun:test";
|
|
import { createContext, type HandlerContext } from "@outfitter/contracts";
|
|
import { myHandler } from "../handlers/my-handler.js";
|
|
|
|
describe("myHandler", () => {
|
|
let ctx: HandlerContext;
|
|
|
|
beforeEach(() => {
|
|
ctx = createContext({});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Success Cases
|
|
// ============================================================================
|
|
|
|
test("returns success for valid input", async () => {
|
|
const result = await myHandler({ id: "valid-id" }, ctx);
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
expect(result.value).toMatchObject({
|
|
id: "valid-id",
|
|
// Add expected properties
|
|
});
|
|
});
|
|
|
|
test("returns success with optional parameters", async () => {
|
|
const result = await myHandler(
|
|
{ id: "valid-id", includeDeleted: true },
|
|
ctx
|
|
);
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
// Assert on optional behavior
|
|
});
|
|
|
|
// ============================================================================
|
|
// Error Cases
|
|
// ============================================================================
|
|
|
|
test("returns NotFoundError for missing resource", async () => {
|
|
const result = await myHandler({ id: "missing" }, ctx);
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
expect(result.error._tag).toBe("NotFoundError");
|
|
expect(result.error.resourceType).toBe("resource");
|
|
expect(result.error.resourceId).toBe("missing");
|
|
});
|
|
|
|
test("returns ValidationError for empty id", async () => {
|
|
const result = await myHandler({ id: "" }, ctx);
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
expect(result.error._tag).toBe("ValidationError");
|
|
});
|
|
|
|
test("returns ValidationError for missing required field", async () => {
|
|
const result = await myHandler({} as any, ctx);
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
expect(result.error._tag).toBe("ValidationError");
|
|
});
|
|
|
|
// ============================================================================
|
|
// Edge Cases
|
|
// ============================================================================
|
|
|
|
test("handles special characters in id", async () => {
|
|
const result = await myHandler({ id: "user-123/test" }, ctx);
|
|
|
|
// Assert expected behavior
|
|
});
|
|
|
|
test("respects cancellation signal", async () => {
|
|
const controller = new AbortController();
|
|
const ctxWithSignal = createContext({ signal: controller.signal });
|
|
|
|
controller.abort();
|
|
const result = await myHandler({ id: "valid-id" }, ctxWithSignal);
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
expect(result.error._tag).toBe("CancelledError");
|
|
});
|
|
});
|
|
```
|
|
|
|
## Checklist
|
|
|
|
- [ ] Test success cases with valid input
|
|
- [ ] Test all error types in handler signature
|
|
- [ ] Test validation errors for invalid/missing input
|
|
- [ ] Test edge cases (special characters, empty arrays, etc.)
|
|
- [ ] Test cancellation if handler supports it
|
|
- [ ] Use `createContext({})` for test context
|
|
- [ ] Check `result.isOk()` / `result.isErr()` before accessing value/error
|
|
- [ ] Use `_tag` for error type discrimination
|
|
|
|
## Result Assertions
|
|
|
|
### Success Assertions
|
|
|
|
```typescript
|
|
// Check success
|
|
expect(result.isOk()).toBe(true);
|
|
|
|
// Access value (type-safe after isOk check)
|
|
expect(result.value.id).toBe("expected-id");
|
|
|
|
// Match object structure
|
|
expect(result.value).toMatchObject({
|
|
id: "expected-id",
|
|
name: expect.any(String),
|
|
});
|
|
|
|
// Array assertions
|
|
expect(result.value.items).toHaveLength(3);
|
|
expect(result.value.items[0]).toMatchObject({ type: "expected" });
|
|
```
|
|
|
|
### Error Assertions
|
|
|
|
```typescript
|
|
// Check error
|
|
expect(result.isErr()).toBe(true);
|
|
|
|
// Check error type
|
|
expect(result.error._tag).toBe("NotFoundError");
|
|
|
|
// Check error category
|
|
expect(result.error.category).toBe("not_found");
|
|
|
|
// Check error properties
|
|
expect(result.error.resourceType).toBe("user");
|
|
expect(result.error.resourceId).toBe("123");
|
|
|
|
// Check error message
|
|
expect(result.error.message).toContain("not found");
|
|
|
|
// Check error details
|
|
expect(result.error.details).toMatchObject({
|
|
field: "email",
|
|
});
|
|
```
|
|
|
|
## Testing with Mock Logger
|
|
|
|
```typescript
|
|
import { createMockLogger } from "@outfitter/testing";
|
|
|
|
test("logs debug messages", async () => {
|
|
const mockLogger = createMockLogger();
|
|
const ctx = createContext({ logger: mockLogger });
|
|
|
|
await myHandler({ id: "123" }, ctx);
|
|
|
|
expect(mockLogger.calls.debug).toContainEqual([
|
|
"Processing",
|
|
{ id: "123" },
|
|
]);
|
|
});
|
|
```
|
|
|
|
## Testing with Fixtures
|
|
|
|
```typescript
|
|
import { createFixture } from "@outfitter/testing";
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
settings: { theme: string };
|
|
}
|
|
|
|
const createUser = createFixture<User>({
|
|
id: "user-1",
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
settings: { theme: "light" },
|
|
});
|
|
|
|
test("processes user with custom settings", async () => {
|
|
const user = createUser({ settings: { theme: "dark" } });
|
|
// user.name is still "Test User" (deep merge)
|
|
// user.settings.theme is "dark"
|
|
});
|
|
```
|
|
|
|
## Testing with Temporary Directories
|
|
|
|
```typescript
|
|
import { withTempDir } from "@outfitter/testing";
|
|
|
|
test("writes config file", async () => {
|
|
await withTempDir(async (dir) => {
|
|
const ctx = createContext({ workspaceRoot: dir });
|
|
const result = await writeConfigHandler({ data: { key: "value" } }, ctx);
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
|
|
const content = await Bun.file(`${dir}/config.json`).json();
|
|
expect(content).toEqual({ key: "value" });
|
|
});
|
|
});
|
|
```
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
# All tests
|
|
bun test
|
|
|
|
# Single file
|
|
bun test src/__tests__/my-handler.test.ts
|
|
|
|
# Watch mode
|
|
bun test --watch
|
|
|
|
# With coverage
|
|
bun test --coverage
|
|
|
|
# Filter by name
|
|
bun test --filter "returns success"
|
|
```
|