AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Write Unit Tests

skill-kennguyen887-agent-foundation-write-unit-tests · by kennguyen887

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.

No reviews yet
0 installs
19 views
0.0% view→install

Install

$ agentstack add skill-kennguyen887-agent-foundation-write-unit-tests

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution Used

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-kennguyen887-agent-foundation-write-unit-tests)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
24d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Write Unit Tests? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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](./write-service-code.md) §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; `/` 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//
    -service.spec.ts     # service tests
    -commands.spec.ts    # command handlers
    -queries.spec.ts     # query handlers (split -simple / -complex if large)
    -events.spec.ts      # event handlers
    dto-validation.spec.ts       # DTO validation

| Testing | File | |---|---| | Service | -service.spec.ts | | Command handlers | -commands.spec.ts | | Query handlers | -queries.spec.ts (or -simple / -complex) | | Event handlers | -events.spec.ts | | Mixed CQRS handlers | -handlers.spec.ts | | DTO validation | dto-validation.spec.ts | | Controller | -controller.spec.ts |

Specs mirror src/modules//. 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/': '/libs//src', ... (mirror tsconfig paths) */ },
};
module.exports = {
  rootDir: './', maxWorkers: '50%', testTimeout: 30_000,
  collectCoverageFrom: ['./src/**/*.(t|j)s'],
  coveragePathIgnorePatterns: ['.module.ts', '/src/config/*', '/src/main.ts', '.mock.ts'],
  projects: [
    { ...sharedConfig, displayName: 'unit', setupFiles: ['reflect-metadata'],
      testMatch: ['/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](./write-service-code.md) §8 harness).

  • Run one file: pnpm jest --selectProjects unit --testPathPattern ''.

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 = {};
  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 {
  const em = { ...createMockRepository(), getRepository: jest.fn().mockReturnValue(createMockRepository()),
    transaction: jest.fn() } as Record;
  em.transaction.mockImplementation(async (cb: any) => cb(em));   // tx → calls back with the EM
  return em;
}

export function createMockDataSource(queryBuilder?: Record) {
  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 = {}) => {
  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(Handler: Type, 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;
  let listingService: ReturnType;

  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|null> {
  const data: Record = {};
  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 {
  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](./write-service-code.md) §8 and [code-conventions](./code-conventions.md) §4.

Verification

  • A spec sits in test/unit/modules// 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 '' passes; coverage report run on new code.

Related

  • [write-service-code](./write-service-code.md) — §8 is the integration/boundary layer (real DB);

the rest is the production code these tests exercise.

  • [structure-a-backend-service](./structure-a-backend-service.md) — module/handler/DTO layout the

specs mirror.

  • [code-conventions](./code-conventions.md) — AAA, faker, it.each, matchers.
  • CLAUDE.mdTesting Rules / HTTP-layer testing rule (when a repo is

integration-only and skips the unit layer).

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.