Agent skill
test-controller
Test controllers that handle HTTP requests. Use when testing controller methods that call services and return responses. Triggers on "test controller", "test note controller".
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/test-controller
SKILL.md
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
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
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
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
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
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
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
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
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
// 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 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 controllerstest-routes- Integration testing with routestest-middleware- Testing middleware
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?