Write unit tests
Use when writing isolated unit tests (mocked deps, no DB) for CQRS handlers, services, or DTOs. Mock factories, a handler testing-module helper, DTO validation, the AAA pattern, entity builders. TS/NestJS/Jest examples.From its SKILL.md
npx -y skills add kennguyen887/agent-foundation --skill write-unit-testsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
15.4 KB, ~3.7k tokens by cl100k_base, as published. Nobody here has run it
When to use
Reach for this when writing isolated unit tests — fast tests that mock every dependency (DB, buses, external services) and never boot the app. Use it for CQRS command/query/event handlers, service classes, and request-DTO validation, or to stand up the unit-test harness in a new service.
Two test layers — don't confuse them:
| Layer | Boots app? | DB | Use for | Doc |
|---|---|---|---|---|
| Unit (this doc) | no | all mocked | handler/service/DTO logic in isolation, fast | here |
| Integration / e2e | yes | real test DB | the use-case through the real transport/HTTP boundary | write-service-code §8 |
A repo runs one or both (two jest projects — see below). Follow your repo's established setup: some repos test integration-only (the global HTTP-layer testing rule — skip a unit test if an integration test already pins the contract); others keep a full unit layer. Don't bolt a unit layer onto an integration-only repo (or vice versa) without agreeing it with the team.
Examples use a neutral listing domain; <Module>/<Entity> are placeholders.
Steps
1. Layout & file naming
test/unit/
test-utils.ts # all mock factory functions (§3)
helpers/
validate-dto.ts # flattenValidationErrors helper (§4, Pattern 3)
factories/
index.ts # re-exports every builder (one-line imports)
listing.factory.ts # buildListing, buildListingWithItems, ...
factories.smoke.spec.ts # smoke test that every builder runs
modules/<module>/
<module>-service.spec.ts # service tests
<module>-commands.spec.ts # command handlers
<module>-queries.spec.ts # query handlers (split -simple / -complex if large)
<module>-events.spec.ts # event handlers
dto-validation.spec.ts # DTO validation
| Testing | File |
|---|---|
| Service | <module>-service.spec.ts |
| Command handlers | <module>-commands.spec.ts |
| Query handlers | <module>-queries.spec.ts (or -simple / -complex) |
| Event handlers | <module>-events.spec.ts |
| Mixed CQRS handlers | <module>-handlers.spec.ts |
| DTO validation | dto-validation.spec.ts |
| Controller | <module>-controller.spec.ts |
Specs mirror src/modules/<module>/. Split a spec past ~800 lines by complexity. Import mocks from
the relative ../../test-utils, never an absolute path.
2. Jest config — two projects
One config, two projects so unit and integration run (and are selectable) separately:
const sharedConfig = {
moduleFileExtensions: ['js', 'json', 'ts'],
transform: { '^.+\\.(t|j)s$': ['@swc/jest', { /* decorators on */ }] },
moduleNameMapper: { /* '@org/<lib>': '<rootDir>/libs/<lib>/src', ... (mirror tsconfig paths) */ },
};
module.exports = {
rootDir: './', maxWorkers: '50%', testTimeout: 30_000,
collectCoverageFrom: ['./src/**/*.(t|j)s'],
coveragePathIgnorePatterns: ['.module.ts', '<rootDir>/src/config/*', '<rootDir>/src/main.ts', '.mock.ts'],
projects: [
{ ...sharedConfig, displayName: 'unit', setupFiles: ['reflect-metadata'],
testMatch: ['<rootDir>/test/unit/**/*.spec.ts'] },
{ ...sharedConfig, displayName: 'integration', setupFilesAfterEnv: ['./test/setup-app.ts'],
testRegex: 'test.e2e.ts' },
],
};
setupFiles: ['reflect-metadata']on the unit project — NestJS decorator metadata without bootstrapping the app.setupFilesAfterEnv: ['./test/setup-app.ts']on integration boots the app once (the write-service-code §8 harness).- Run one file:
pnpm jest --selectProjects unit --testPathPattern '<your-file>'.
3. Shared mock factories (test/unit/test-utils.ts)
One module of small factory functions keeps specs fast and identical. The reusable core (copy as-is;
chain methods return this, terminal methods return configurable defaults):
import { EventBus, CommandBus, QueryBus } from '@nestjs/cqrs';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { DataSource } from 'typeorm';
import { Type } from '@nestjs/common';
export function createMockQueryBuilder(getOneResult?: any, getOneOrFailResult?: any) {
const qb: Record<string, jest.Mock> = {};
for (const m of ['select','addSelect','where','andWhere','orWhere','innerJoin','leftJoin',
'innerJoinAndSelect','leftJoinAndSelect','orderBy','addOrderBy','skip','take','limit','offset',
'groupBy','having','setParameter','from','update','set','insert','into','values','withDeleted','distinct']) {
qb[m] = jest.fn().mockReturnValue(qb); // chain → return self
}
qb.getOne = jest.fn().mockResolvedValue(getOneResult ?? null);
qb.getOneOrFail = jest.fn().mockResolvedValue(getOneOrFailResult ?? getOneResult);
qb.getMany = jest.fn().mockResolvedValue([]);
qb.getManyAndCount = jest.fn().mockResolvedValue([[], 0]);
qb.getCount = jest.fn().mockResolvedValue(0);
qb.getRawOne = jest.fn().mockResolvedValue(null);
qb.getRawMany = jest.fn().mockResolvedValue([]);
qb.execute = jest.fn().mockResolvedValue(undefined);
return qb;
}
export function createMockRepository() {
return {
findOne: jest.fn(), findOneBy: jest.fn(), find: jest.fn(),
findAndCount: jest.fn().mockResolvedValue([[], 0]),
save: jest.fn().mockImplementation((e: any) => Promise.resolve(e)),
create: jest.fn().mockImplementation((e: any) => e),
insert: jest.fn().mockResolvedValue({ identifiers: [{ id: 'mock-id' }] }),
update: jest.fn().mockResolvedValue({ affected: 1 }),
delete: jest.fn().mockResolvedValue({ affected: 1 }),
count: jest.fn().mockResolvedValue(0),
createQueryBuilder: jest.fn(),
};
}
export function createMockEntityManager(): Record<string, jest.Mock> {
const em = { ...createMockRepository(), getRepository: jest.fn().mockReturnValue(createMockRepository()),
transaction: jest.fn() } as Record<string, jest.Mock>;
em.transaction.mockImplementation(async (cb: any) => cb(em)); // tx → calls back with the EM
return em;
}
export function createMockDataSource(queryBuilder?: Record<string, jest.Mock>) {
const qb = queryBuilder ?? createMockQueryBuilder();
const manager = createMockEntityManager();
manager.createQueryBuilder.mockReturnValue(qb);
return {
createQueryBuilder: jest.fn().mockReturnValue(qb),
getRepository: jest.fn().mockReturnValue(createMockRepository()),
transaction: jest.fn().mockImplementation(async (cb: any) => cb(manager)),
manager,
};
}
export const createMockLogger = () => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn(), verbose: jest.fn(), log: jest.fn() });
export const createMockEventBus = () => ({ publish: jest.fn(), publishAll: jest.fn() });
export const createMockCommandBus = () => ({ execute: jest.fn() });
export const createMockQueryBus = () => ({ execute: jest.fn() });
export const createMockQueue = () => ({ add: jest.fn(), process: jest.fn(), on: jest.fn(), getJob: jest.fn(), close: jest.fn() });
export const createMockConfigService = (overrides: Record<string, any> = {}) => {
const defaults = { 'app.baseUrl': 'http://localhost:3000', 'app.env': 'test', ...overrides };
return { get: jest.fn().mockImplementation((k: string) => defaults[k]) };
};
export const mockProvider = (token: any, value: any) => ({ provide: token, useValue: value });
// One CQRS handler, all common deps pre-wired. Add only the providers your handler needs.
export async function createHandlerTestingModule<H>(Handler: Type<H>, extraProviders: any[] = []) {
const qb = createMockQueryBuilder();
const dataSource = createMockDataSource(qb);
const eventBus = createMockEventBus(), commandBus = createMockCommandBus(), queryBus = createMockQueryBus();
const configService = createMockConfigService(), logger = createMockLogger();
const module = await Test.createTestingModule({
providers: [
Handler,
mockProvider(DataSource, dataSource),
mockProvider('winston', logger), // winston is injected by the string token 'winston'
mockProvider(EventBus, eventBus), mockProvider(CommandBus, commandBus), mockProvider(QueryBus, queryBus),
mockProvider(ConfigService, configService),
...extraProviders,
],
}).compile();
return { module, handler: module.get(Handler), qb, dataSource, eventBus, commandBus, queryBus, configService, logger };
}
Domain-service mocks: one per service, same shape — a plain object of jest.fn()s. Keep them in
test-utils.ts so every spec shares them:
export const createMockListingService = () => ({
getListingById: jest.fn(),
publishListing: jest.fn().mockResolvedValue(undefined),
// ...one jest.fn() per public method
});
4. The three patterns
Pattern 1 — CQRS handler (the fast path). createHandlerTestingModule wires DataSource, the
three buses, ConfigService, and the logger; you add only the rest.
import { PublishListingHandler, PublishListingCommand } from '../../../../src/modules/listing/commands/publish-listing';
import { ListingService } from '../../../../src/modules/listing/listing.service';
import { AppNotFoundException } from '@org/infra-exception';
import { createHandlerTestingModule, createMockListingService, mockProvider } from '../../test-utils';
describe('PublishListingHandler', () => {
let handler: PublishListingHandler;
let qb: ReturnType<typeof import('../../test-utils').createMockQueryBuilder>;
let listingService: ReturnType<typeof createMockListingService>;
beforeEach(async () => {
listingService = createMockListingService();
({ handler, qb } = await createHandlerTestingModule(PublishListingHandler, [
mockProvider(ListingService, listingService),
]));
});
afterEach(() => jest.resetAllMocks());
it('publishes when the listing exists', async () => {
// Arrange
qb.getOne.mockResolvedValueOnce({ id: '1', status: 'DRAFT' });
// Act
const result = await handler.execute(new PublishListingCommand('1'));
// Assert — specific value, not toBeTruthy()
expect(result.status).toBe('PUBLISHED');
});
it('throws when the listing is missing', async () => {
qb.getOne.mockResolvedValueOnce(null);
await expect(handler.execute(new PublishListingCommand('missing'))).rejects.toThrow(AppNotFoundException);
});
});
Pattern 2 — service (manual TestingModule). Services have more deps; build the module with
mockProvider. The repository token is getRepositoryToken(Entity); the logger token is the string
'winston'.
const qb = createMockQueryBuilder();
const repo = createMockRepository(); repo.createQueryBuilder.mockReturnValue(qb);
const module = await Test.createTestingModule({
providers: [
ListingService,
mockProvider(DataSource, createMockDataSource(qb)),
mockProvider(getRepositoryToken(Listing), repo),
mockProvider('winston', createMockLogger()),
// ...other deps
],
}).compile();
const service = module.get(ListingService);
Pattern 3 — DTO validation. plainToInstance + validate + a flattenValidationErrors helper
that mirrors the API's error shape:
// test/unit/helpers/validate-dto.ts
export function flattenValidationErrors(errors: ValidationError[]): Record<string, Record<string,string>|null> {
const data: Record<string, any> = {};
const walk = (errs: ValidationError[], parent?: string) => errs.forEach((e) => {
const key = parent ? `${parent}.${e.property}` : e.property;
if (e.constraints) data[key] = e.constraints;
else if (e.children?.length) walk(e.children, key);
});
walk(errors);
return data;
}
// spec
it('rejects an empty body', async () => {
const dto = plainToInstance(CreateListingRequestDto, {});
const data = flattenValidationErrors(await validate(dto, { whitelist: true }));
expect(data).toMatchObject({ title: { isNotEmpty: expect.any(String) } });
});
5. Entity builders (test/unit/factories/)
In-memory builders (they do not touch the DB — that's the integration layer's job). Constructor
- spread overrides + a sensible default for every field; override only what the test asserts. Use a faker lib for incidental values.
export function buildListing(overrides: Partial<Listing> = {}): Listing {
return new Listing({
id: overrides.id ?? uuidv4(), status: ListingStatus.DRAFT, title: faker.commerce.productName(),
price: 10, currency: 'SGD', isDeleted: false, createdAt: new Date(), updatedAt: new Date(),
...overrides,
});
}
Re-export every builder from factories/index.ts (one-line imports), and keep a
factories.smoke.spec.ts that just calls each builder so a drifted default fails fast.
6. jest.mock patterns
Place jest.mock() after imports, before describe (Jest hoists them; this is the convention).
// timezone-sensitive: keep the real lib but pin plugins
jest.mock('dayjs', () => { const d = jest.requireActual('dayjs'); d.extend(jest.requireActual('dayjs/plugin/utc')); d.extend(jest.requireActual('dayjs/plugin/timezone')); return d; });
// stub a couple of shared utils, keep the rest real
jest.mock('../../../../src/shared/utils', () => ({ ...jest.requireActual('../../../../src/shared/utils'), getDeepLink: jest.fn().mockResolvedValue('https://x.test') }));
7. Assertions & cleanup
- QueryBuilder:
expect(qb.where).toHaveBeenCalledWith('listing.id = :id', { id: '1' }),expect(qb.getOne).toHaveBeenCalled(). - Events:
expect(eventBus.publish).toHaveBeenCalledWith(expect.any(ListingPublishedEvent))orexpect.objectContaining({ listingId: '1' }). - Transactions: the mock
dataSource.transaction()calls back withdataSource.manager— setdataSource.manager.findOne.mockResolvedValueOnce(...)then assertdataSource.manager.save. - Cleanup:
afterEach(() => jest.resetAllMocks())—resetAllMocks(notclearAllMocks) so implementations reset too and tests don't pollute each other. - AAA, one Act, faker,
it.eachfor branch variants, and specific-value matchers (toBe/toMatchObject/toEqual/arrayContaining) — same as write-service-code §8 and code-conventions §4.
Verification
- A spec sits in
test/unit/modules/<module>/with the naming convention; imports mocks from../../test-utils; usescreateHandlerTestingModule(handlers) or manualTest.createTestingModule(services). - Entity data comes from
../../factoriesbuilders; incidental values from faker. - Every spec ends with
afterEach(() => jest.resetAllMocks()). - Tests cover happy path + error/not-found + business edge cases, asserting specific values.
pnpm jest --selectProjects unit --testPathPattern '<file>'passes; coverage report run on new code.
Related
- write-service-code — §8 is the integration/boundary layer (real DB); the rest is the production code these tests exercise.
- structure-a-backend-service — module/handler/DTO layout the specs mirror.
- code-conventions — AAA, faker,
it.each, matchers. CLAUDE.md— Testing Rules / HTTP-layer testing rule (when a repo is integration-only and skips the unit layer).
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most docs writing skills give in ~3.7k tokens
Counted across 1,637 of the 3,044 authors here whose files we hold, read 2026-08-07
- Announce the skill at startin 54 of 1637, across 26 files
- Convert legacy doc files before editingin 45 of 1637, across 7 files
- Predict questions readers might askin 42 of 1637, across 4 files
- Generate clarifying questions for initial contextin 42 of 1637, across 3 files
- Create document scaffold with placeholder textin 42 of 1637, across 3 files
- Brainstorm content options for each sectionin 42 of 1637, across 3 files
- Test the document with a fresh context-less instancein 42 of 1637, across 3 files
- Include exact file paths in every taskin 42 of 1637, across 15 files
- Ask interview questions one at a timein 42 of 1637, across 27 files
- Apply surgical edits during refinementin 41 of 1637, across 2 files
- Offer structured workflow or freeformin 40 of 1637, across 1 file
- Ask for document meta-contextin 40 of 1637, across 2 files
Said here and by no other author read
- never boot the application
- follow repository testing setup
- import mocks via relative paths
- split large spec files by complexity
- run unit and integration tests separately
- use shared mock factory functions
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.