← Back to list

test-controller
by madooei
This is a template for a backend service built with TypeScript and Hono.js. It provides a basic structure for creating RESTful APIs.
⭐ 1🍴 0📅 Jan 5, 2026
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
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
Score
Total Score
70/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
✓説明文
100文字以上の説明がある
+10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon