Back to list
madooei

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 controllers
  • test-routes - Integration testing with routes
  • test-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