agentsclimarketplace

Write unit tests

Skill kennguyen887/agent-foundation/skills/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

Install
npx -y skills add kennguyen887/agent-foundation --skill write-unit-tests

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

LayerBoots app?DBUse forDoc
Unit (this doc)noall mockedhandler/service/DTO logic in isolation, fasthere
Integration / e2eyesreal test DBthe use-case through the real transport/HTTP boundarywrite-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
TestingFile
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 validationdto-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)) or expect.objectContaining({ listingId: '1' }).
  • Transactions: the mock dataSource.transaction() calls back with dataSource.manager — set dataSource.manager.findOne.mockResolvedValueOnce(...) then assert dataSource.manager.save.
  • Cleanup: afterEach(() => jest.resetAllMocks())resetAllMocks (not clearAllMocks) so implementations reset too and tests don't pollute each other.
  • AAA, one Act, faker, it.each for 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; uses createHandlerTestingModule (handlers) or manual Test.createTestingModule (services).
  • Entity data comes from ../../factories builders; 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.mdTesting 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.

Keep looking

Skills are one crate of 326,790. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.