← スキル一覧に戻る
test-controller
madooei / backend-template
⭐ 1🍴 0📅 2026年1月5日
Test controllers that handle HTTP requests. Use when testing controller methods that call services and return responses. Triggers on "test controller", "test note controller".
SKILL.md
---
name: test-controller
description: Test controllers that handle HTTP requests. Use when testing controller methods that call services and return responses. Triggers on "test controller", "test note controller".
---
# Test Controller
Tests controllers by mocking services and Hono context.
## Quick Reference
**Location**: `tests/controllers/{entity-name}.controller.test.ts`
**Key technique**: Mock service methods, create mock Hono context
## Test Structure
```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { {Entity}Controller } from "@/controllers/{entity-name}.controller";
import type { {Entity}Service } from "@/services/{entity-name}.service";
import type {
Create{Entity}Type,
Update{Entity}Type,
{Entity}QueryParamsType,
{Entity}Type,
} from "@/schemas/{entity-name}.schema";
import type { AuthenticatedUserContextType } from "@/schemas/user.schemas";
import type { PaginatedResultType, EntityIdParamType } from "@/schemas/shared.schema";
import { NotFoundError } from "@/errors";
// Mock context helper
interface MockContextConfig {
user?: AuthenticatedUserContextType;
validatedQuery?: {Entity}QueryParamsType;
validatedParams?: EntityIdParamType;
validatedBody?: Create{Entity}Type | Update{Entity}Type;
}
const createMockContext = (config: MockContextConfig = {}) => {
const mockJson = vi.fn((data) => data);
return {
var: {
user: config.user || ({} as AuthenticatedUserContextType),
validatedQuery: config.validatedQuery || ({} as {Entity}QueryParamsType),
validatedParams: config.validatedParams || ({} as EntityIdParamType),
validatedBody: config.validatedBody || ({} as Create{Entity}Type | Update{Entity}Type),
},
json: mockJson,
} as any;
};
// Mock service
const mockService = {
getAll: vi.fn(),
getById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
describe("{Entity}Controller", () => {
let controller: {Entity}Controller;
let user: AuthenticatedUserContextType;
let sample{Entity}: {Entity}Type;
beforeEach(() => {
controller = new {Entity}Controller(mockService as unknown as {Entity}Service);
user = { userId: "user-1", globalRole: "user" };
sample{Entity} = {
id: "{entity}-1",
// ... entity fields
createdBy: user.userId,
createdAt: new Date(),
updatedAt: new Date(),
};
});
afterEach(() => {
vi.clearAllMocks();
});
// Test groups...
});
```
## Test Categories
### 1. getAll Tests
```typescript
describe("getAll", () => {
it("returns items from service and calls c.json", async () => {
const query: {Entity}QueryParamsType = { page: 1, limit: 10 };
const result: PaginatedResultType<{Entity}Type> = {
data: [sample{Entity}],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
};
mockService.getAll.mockResolvedValue(result);
const mockCtx = createMockContext({ user, validatedQuery: query });
const response = await controller.getAll(mockCtx);
expect(mockService.getAll).toHaveBeenCalledWith(query, user);
expect(mockCtx.json).toHaveBeenCalledWith(result);
expect(response).toEqual(result);
});
});
```
### 2. getById Tests
```typescript
describe("getById", () => {
it("returns item when found", async () => {
const params: EntityIdParamType = { id: sample{Entity}.id };
mockService.getById.mockResolvedValue(sample{Entity});
const mockCtx = createMockContext({ user, validatedParams: params });
const response = await controller.getById(mockCtx);
expect(mockService.getById).toHaveBeenCalledWith(params.id, user);
expect(mockCtx.json).toHaveBeenCalledWith(sample{Entity});
});
it("throws NotFoundError when not found", async () => {
const params: EntityIdParamType = { id: "non-existent" };
mockService.getById.mockResolvedValue(null);
const mockCtx = createMockContext({ user, validatedParams: params });
await expect(controller.getById(mockCtx)).rejects.toThrow(NotFoundError);
expect(mockCtx.json).not.toHaveBeenCalled();
});
});
```
### 3. create Tests
```typescript
describe("create", () => {
it("creates item and returns it", async () => {
const createDto: Create{Entity}Type = { /* data */ };
const created{Entity} = { ...sample{Entity}, ...createDto };
mockService.create.mockResolvedValue(created{Entity});
const mockCtx = createMockContext({ user, validatedBody: createDto });
const response = await controller.create(mockCtx);
expect(mockService.create).toHaveBeenCalledWith(createDto, user);
expect(mockCtx.json).toHaveBeenCalledWith(created{Entity});
});
});
```
### 4. update Tests
```typescript
describe("update", () => {
it("updates item and returns it", async () => {
const params: EntityIdParamType = { id: sample{Entity}.id };
const updateDto: Update{Entity}Type = { /* data */ };
const updated{Entity} = { ...sample{Entity}, ...updateDto };
mockService.update.mockResolvedValue(updated{Entity});
const mockCtx = createMockContext({
user,
validatedParams: params,
validatedBody: updateDto,
});
const response = await controller.update(mockCtx);
expect(mockService.update).toHaveBeenCalledWith(params.id, updateDto, user);
expect(mockCtx.json).toHaveBeenCalledWith(updated{Entity});
});
it("throws NotFoundError when not found", async () => {
const params: EntityIdParamType = { id: "non-existent" };
mockService.update.mockResolvedValue(null);
const mockCtx = createMockContext({
user,
validatedParams: params,
validatedBody: { /* data */ },
});
await expect(controller.update(mockCtx)).rejects.toThrow(NotFoundError);
});
});
```
### 5. delete Tests
```typescript
describe("delete", () => {
it("deletes item and returns success message", async () => {
const params: EntityIdParamType = { id: sample{Entity}.id };
mockService.delete.mockResolvedValue(true);
const mockCtx = createMockContext({ user, validatedParams: params });
await controller.delete(mockCtx);
expect(mockService.delete).toHaveBeenCalledWith(params.id, user);
expect(mockCtx.json).toHaveBeenCalledWith({
message: "{Entity} deleted successfully",
});
});
it("throws NotFoundError when not found", async () => {
const params: EntityIdParamType = { id: "non-existent" };
mockService.delete.mockResolvedValue(false);
const mockCtx = createMockContext({ user, validatedParams: params });
await expect(controller.delete(mockCtx)).rejects.toThrow(NotFoundError);
});
});
```
## Key Patterns
### Mock Context Factory
```typescript
const createMockContext = (config: MockContextConfig = {}) => {
const mockJson = vi.fn((data) => data);
return {
var: {
user: config.user,
validatedQuery: config.validatedQuery,
validatedParams: config.validatedParams,
validatedBody: config.validatedBody,
},
json: mockJson,
} as any;
};
```
### Mock Service Object
```typescript
const mockService = {
getAll: vi.fn(),
getById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
};
// Inject into controller
controller = new Controller(mockService as unknown as Service);
```
### Assertions
```typescript
// Verify service called correctly
expect(mockService.getById).toHaveBeenCalledWith(id, user);
// Verify response
expect(mockCtx.json).toHaveBeenCalledWith(expectedData);
// Verify error thrown
await expect(controller.method(ctx)).rejects.toThrow(NotFoundError);
// Verify json NOT called on error
expect(mockCtx.json).not.toHaveBeenCalled();
```
## Complete Example
See [REFERENCE.md](REFERENCE.md) for a complete controller test.
## What NOT to Do
- Do NOT test actual HTTP requests (that's integration testing)
- Do NOT use real services (mock them)
- Do NOT forget to clear mocks between tests
- Do NOT test validation (that's middleware's job)
- Do NOT test authorization (that's service's job)
## See Also
- `create-controller` - Creating controllers
- `test-routes` - Integration testing with routes
- `test-middleware` - Testing middleware