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".

Stars 163
Forks 31

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

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 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

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results