From nestjs-clean-arch
Activates this skill when a developer asks to "write a test", "create unit test", "test a use case", "test a controller", "create stub", "add test for", "how to test", "write spec", "mock a repository", "mock a use case", or "set up a TestingModule". Provides the exact testing conventions, patterns, and file structures used in this NestJS Clean Architecture project so that all tests are consistent with the existing test suite.
npx claudepluginhub tuannguyen151/foxdemon-plugins --plugin nestjs-clean-archThis skill uses the workspace's default tool permissions.
A complete guide to writing consistent, correct tests across all layers of this NestJS Clean Architecture project.
Builds reliable Jest test suites for NestJS modules, services, and controllers covering unit, integration, and e2e tests. Use for TestModule setup, mocking providers, database fakes, and debugging flaky tests.
Provides strategies for writing maintainable unit, integration, and E2E tests using AAA pattern, mocking, test naming, and organization. Useful for TDD and test infrastructure.
Writes TDD tests supporting Jest, Cypress, Detox, PHPUnit, PyTest, and Go testing. Adds unit, integration, E2E tests to improve coverage on existing code.
Share bugs, ideas, or general feedback.
A complete guide to writing consistent, correct tests across all layers of this NestJS Clean Architecture project.
For full TypeScript code examples, read the reference files:
references/use-case-tests.md— TestingModule setup, repository mocking, error-throwing patternsreferences/controller-tests.md— PoliciesGuard override, DTO validation, presenter assertionreferences/stub-patterns.md— Stub factory functions, overrides, when to use stubs vs inline data
Mirror src/ structure exactly inside test/:
| Source file | Test file |
|---|---|
src/use-cases/tasks/create-task.use-case.ts | test/use-cases/tasks/create-task.use-case.spec.ts |
src/adapters/controllers/tasks/tasks.controller.ts | test/adapters/controllers/tasks/tasks.controller.spec.ts |
src/adapters/controllers/tasks/dto/create-task.dto.ts | test/adapters/controllers/tasks/dto/create-task.dto.spec.ts |
src/infrastructure/databases/postgresql/repositories/task.repository.ts | test/infrastructure/databases/postgresql/repositories/task.repository.spec.ts |
Place stubs in test/stubs/ and mock objects in test/mocks/.
test/use-cases/)Test the business logic in isolation. Mock all repositories and services injected by the use case. Assert that:
test/adapters/controllers/)Two subtypes exist:
Controller method tests ([feature].controller.spec.ts) — Mock all use cases, override PoliciesGuard, verify that the controller delegates to use cases correctly and wraps results in the correct Presenter.
DTO validation tests (dto/[name].dto.spec.ts) — Use class-validator's validate() directly. No TestingModule needed.
test/infrastructure/)Test repository implementations, services (bcrypt, jwt, casl), filters, interceptors, and strategies against real or mocked dependencies.
[name].spec.ts (always .spec.ts, never .test.ts)test/stubs/[entity].stub.tstest/mocks/[category]/[name].mock.tsUse these prefixes consistently:
| Prefix | Purpose | Example |
|---|---|---|
input | Input parameters passed to the method under test | inputPayload, inputUserId |
mock | Pre-created stub or mock return value | mockTask, mockUser |
actual | Actual return value from the method under test | actualResult |
expected | Expected value for assertion | expectedTasks |
Note: Existing tests in this project sometimes use plain
resultortask— prefer the prefixed naming for new tests.
describe / it namingdescribe('CreateTaskUseCase', () => { // Class name
describe('execute', () => { // Method name (omit if only one public method)
it('should create a task', async () => { // "should <do something>"
it('should throw when task not found', async () => {
Structure every it block in three sections:
it('should return a list of tasks', async () => {
// Arrange
const inputQuery = { userId: 1 }
const mockTasks = [createTaskStub()]
jest.spyOn(taskRepository, 'findTasks').mockResolvedValueOnce(mockTasks)
// Act
const actualResult = await useCase.execute(inputQuery)
// Assert
expect(taskRepository.findTasks).toHaveBeenCalledWith(inputQuery)
expect(actualResult).toEqual(mockTasks)
})
Rules:
jest.spyOn(...).mockResolvedValueOnce(...) for async mocks — Once prevents test bleedmockResolvedValue(...) (without Once) only when the same mock value is needed across multiple calls within a single testjest.spyOn calls in the Arrange section, not in beforeEach (unless shared across all tests in the describe block)| Test type | Providers to mock | Guard override |
|---|---|---|
| Use case with repository | { provide: ITaskRepository, useValue: { method: jest.fn() } } | Not needed |
| Use case with exception service | { provide: IException, useValue: { notFoundException: jest.fn() } } + mockImplementation to throw | Not needed |
| Controller with use cases | { provide: CreateTaskUseCase, useValue: { execute: jest.fn() } } for every use case | overrideGuard(PoliciesGuard).useValue({ canActivate: () => true }) |
| Controller with CASL | { provide: 'ABILITY_FACTORY_INTERFACE', useValue: {} } | Required |
| Auth controller (public routes) | Use cases only — no PoliciesGuard override needed | Not needed |
| DTO validation | None — call validate(dto) directly | Not applicable |
beforeEach vs Per-Test Spy SetupUse beforeEach to build the TestingModule and acquire service references. Set up jest.spyOn inside each it block, not in beforeEach, unless the same mock value applies to every test in the describe block.
// ✅ Correct: spy in the test
it('should return a task', async () => {
const mockTask = createTaskStub()
jest.spyOn(taskRepository, 'findOneTask').mockResolvedValueOnce(mockTask)
const actualResult = await useCase.execute({ id: 1, userId: 1 })
expect(actualResult).toEqual(mockTask)
})
// ❌ Avoid: spy in beforeEach bleeds between tests
beforeEach(() => {
jest.spyOn(taskRepository, 'findOneTask').mockResolvedValue(mockTask)
})
The exception: mockImplementation for exception-throwing services belongs in beforeEach because it applies to the entire describe block and does not set a return value.
Organize tests using nested describe blocks:
describe('UpdateTaskUseCase', () => { // outer: class name
// TestingModule setup in beforeEach here
describe('execute', () => { // inner: method name
it('should update the task and return true', ...)
it('should throw when task not found', ...)
})
})
Use a single flat describe (no inner describe('execute')) when the class has only one public method and the test file is short. See create-task.use-case.spec.ts as an example of the flat style.
Follow this import order in test files:
// 1. NestJS testing utilities
import { Test, type TestingModule } from '@nestjs/testing'
// 2. Third-party (e.g., class-validator, express)
import { validate } from 'class-validator'
// 3. Test utilities (stubs, mocks) — path alias "test/"
import { createTaskStub } from 'test/stubs/task.stub'
// 4. Domain layer imports — @domain/*
import { TaskPriorityEnum } from '@domain/entities/task.entity'
import { ITaskRepository } from '@domain/repositories/task.repository.interface'
// 5. Use case imports — @use-cases/*
import { CreateTaskUseCase } from '@use-cases/tasks/create-task.use-case'
// 6. Adapter imports — @adapters/*
import { PoliciesGuard } from '@adapters/controllers/common/guards/policies.guard'
import { CreateTaskPresenter } from '@adapters/controllers/tasks/presenters/create-tasks.presenter'
import { TasksController } from '@adapters/controllers/tasks/tasks.controller'
When a use case uses IException to throw domain exceptions, configure mockImplementation to actually throw during the test. Do this in beforeEach after acquiring the service reference:
(exceptionsService.notFoundException as unknown as jest.Mock)
.mockImplementation((data: { message: string }) => {
throw new Error(data.message)
})
Then assert with rejects.toThrow(...):
await expect(useCase.execute(payload)).rejects.toThrow('Task not found')
expect(exceptionsService.notFoundException).toHaveBeenCalledWith({
type: 'TaskNotFoundException',
message: 'Task not found',
})
Repository and service interfaces are registered as Symbol tokens, not class tokens:
// ✅ Correct — ITaskRepository is a Symbol
{ provide: ITaskRepository, useValue: { findTasks: jest.fn() } }
// ❌ Wrong — never use a string or a class as the token for domain interfaces
{ provide: 'ITaskRepository', useValue: { findTasks: jest.fn() } }
The only string token in this project is 'ABILITY_FACTORY_INTERFACE' — this one intentionally uses a string.
Retrieve the instance using the same Symbol:
taskRepository = module.get<ITaskRepository>(ITaskRepository)
Infrastructure tests follow the same TestingModule approach but test concrete implementations:
// test/infrastructure/databases/postgresql/repositories/task.repository.spec.ts
describe('TaskRepository', () => {
let repository: TaskRepository
let dataSource: DataSource
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
TaskRepository,
{
provide: getDataSourceToken(),
useValue: mockDataSource,
},
],
}).compile()
repository = module.get<TaskRepository>(TaskRepository)
})
Strategies (e.g., JwtStrategy) require mocking the config service via the mock object in test/mocks/services/:
import { environmentConfigServiceMock } from 'test/mocks/services/environment-config-service.mock'
| Mistake | Correct approach |
|---|---|
provide: 'ITaskRepository' (string) | provide: ITaskRepository (Symbol) |
provide: TaskRepositoryImpl (concrete class) | provide: ITaskRepository (interface Symbol) |
Missing 'ABILITY_FACTORY_INTERFACE' provider | Add { provide: 'ABILITY_FACTORY_INTERFACE', useValue: {} } |
Not overriding PoliciesGuard | Add .overrideGuard(PoliciesGuard).useValue({ canActivate: () => true }) |
expect(result).toEqual(mockTask) without Presenter | expect(result).toEqual(new CreateTaskPresenter(mockTask)) |
mockResolvedValue instead of mockResolvedValueOnce | Use mockResolvedValueOnce unless the same value is needed across multiple calls |
jest.spyOn inside beforeEach for per-test behavior | Move jest.spyOn into each it block |
| Constructing domain entities inline | Import and call stub factory from test/stubs/ |
Wrap expected return values in the actual Presenter constructor — do not compare plain objects:
expect(result).toEqual(new GetListTasksPresenter(mockTask))
expect(result).toEqual(tasks.map((task) => new GetListTasksPresenter(task)))
All test commands run inside the Docker container:
# Run all tests
docker exec -it app-api npm run test
# Run with coverage
docker exec -it app-api npm run test:cov
# Watch a specific file during development
docker exec -it app-api npm run test:watch test/use-cases/tasks/create-task.use-case.spec.ts
# Run end-to-end tests
docker exec -it app-api npm run test:e2e
test/ mirroring the src/ pathtest/stubs/[entity].stub.tsTestingModule in beforeEach — one TestingModule per describe blockPoliciesGuard in controller tests that use @CheckPolicies'ABILITY_FACTORY_INTERFACE' whenever CaslAbilityFactory is in the module's dependency treeit block