本文へ移動
cccskills
無料GitHub で公開

unit-testing

Unit testing patterns with Vitest - mocking, fixtures, isolation, and fast feedback.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md9.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Unit Testing Skill

Fast, isolated tests that verify individual functions and modules. Unit tests run in the CI runner (not Kubernetes) for rapid feedback.


Overview

AspectDetails
Locationcomponents/*/src/**/*.test.ts
FrameworkVitest
Runs InCI runner (fast feedback)
Written ByImplementors (alongside their code)

Test Structure

File Naming

Place test files next to the code they test:

src/model/use-cases/
├── create_user.ts
├── create_user.test.ts      # Unit test for create_user
├── update_user.ts
└── update_user.test.ts

Basic Pattern

// src/model/use-cases/create_user.test.ts
import { describe, it, expect } from 'vitest';
import { createUser } from './create_user';
import type { Dependencies } from '../dependencies';

/**
 * @spec changes/user-management/SPEC.md
 * @issue PROJ-123
 */
describe('createUser', () => {
  describe('AC1: Valid user creation', () => {
    it('creates user when email is unique', async () => {
      // Arrange (Given)
      const mockDeps = createMockDependencies();
      const args = { email: 'test@example.com', name: 'Test User' };

      // Act (When)
      const result = await createUser(mockDeps, args);

      // Assert (Then)
      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.user.email).toBe('test@example.com');
      }
    });
  });
});

Mocking Strategies

Dependency Injection Mocks

For functions that receive dependencies as the first argument (CMDO pattern):

import type { Dependencies } from '../dependencies';

const createMockDependencies = (overrides?: Partial<Dependencies>): Dependencies => ({
  findUserByEmail: async () => null,
  findUserById: async () => null,
  insertUser: async (data) => ({ id: 'mock-id', ...data, createdAt: new Date() }),
  updateUser: async (id, data) => ({ id, ...data, updatedAt: new Date() }),
  ...overrides,
});

// Usage in tests
it('returns error when email exists', async () => {
  const deps = createMockDependencies({
    findUserByEmail: async () => ({ id: '123', email: 'exists@test.com', name: 'Existing' }),
  });

  const result = await createUser(deps, { email: 'exists@test.com', name: 'New' });

  expect(result.success).toBe(false);
});

Vitest Mock Functions

For tracking calls and controlling return values:

import { describe, it, expect, vi } from 'vitest';

it('calls insertUser with correct data', async () => {
  const insertUser = vi.fn().mockResolvedValue({ id: '123', email: 'test@example.com' });
  const deps = createMockDependencies({ insertUser });

  await createUser(deps, { email: 'test@example.com', name: 'Test' });

  expect(insertUser).toHaveBeenCalledOnce();
  expect(insertUser).toHaveBeenCalledWith(
    expect.objectContaining({ email: 'test@example.com' })
  );
});

Module Mocks

For mocking entire modules (use sparingly):

import { vi } from 'vitest';

vi.mock('../services/email', () => ({
  sendEmail: vi.fn().mockResolvedValue({ sent: true }),
}));

import { sendEmail } from '../services/email';

it('sends welcome email after user creation', async () => {
  await createUserWithWelcomeEmail(deps, { email: 'new@test.com' });

  expect(sendEmail).toHaveBeenCalledWith(
    expect.objectContaining({ to: 'new@test.com', template: 'welcome' })
  );
});

Fixtures and Test Data

Factory Functions

Create reusable factories for test data:

// src/__tests__/fixtures/user.ts
import type { User } from '../../types/generated';

export const createTestUser = (overrides?: Partial<User>): User => ({
  id: 'test-user-id',
  email: 'test@example.com',
  name: 'Test User',
  role: 'planner',
  clientId: 'test-client',
  createdAt: new Date('2026-01-01'),
  updatedAt: new Date('2026-01-01'),
  ...overrides,
});

// Usage
it('formats user display name', () => {
  const user = createTestUser({ name: 'John Doe' });
  expect(formatDisplayName(user)).toBe('John Doe');
});

it('uses email when name is empty', () => {
  const user = createTestUser({ name: '', email: 'john@example.com' });
  expect(formatDisplayName(user)).toBe('john');
});

Shared Test Constants

// src/__tests__/fixtures/constants.ts
export const TEST_USER_ID = 'test-user-123';
export const TEST_CLIENT_ID = 'test-client-456';
export const TEST_EMAIL = 'test@example.com';

export const VALID_CREDENTIALS = {
  email: TEST_EMAIL,
  password: 'ValidPass123!',
};

export const INVALID_CREDENTIALS = {
  email: TEST_EMAIL,
  password: 'wrong',
};

Isolation Strategies

Reset State Between Tests

import { describe, it, beforeEach, afterEach, vi } from 'vitest';

describe('userService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  // Tests run in isolation
});

Avoid Shared Mutable State

// BAD: Shared mutable state
let testUser = { id: '1', name: 'Test' };

it('test 1', () => {
  testUser.name = 'Modified'; // Affects other tests!
});

// GOOD: Create fresh data per test
it('test 1', () => {
  const testUser = createTestUser();
  // Modifications don't affect other tests
});

Time Mocking

import { vi, beforeEach, afterEach } from 'vitest';

describe('time-dependent tests', () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-01-15T10:00:00Z'));
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('calculates expiration correctly', () => {
    const token = createToken({ expiresIn: '1h' });
    expect(token.expiresAt).toEqual(new Date('2026-01-15T11:00:00Z'));
  });
});

Testing Async Code

Promises

it('handles async operations', async () => {
  const result = await createUser(deps, validArgs);
  expect(result.success).toBe(true);
});

it('handles rejected promises', async () => {
  const deps = createMockDependencies({
    insertUser: async () => { throw new Error('DB connection failed'); },
  });

  await expect(createUser(deps, validArgs)).rejects.toThrow('DB connection failed');
});

Testing Error Scenarios

it('returns error result for validation failure', async () => {
  const result = await createUser(deps, { email: 'invalid-email', name: '' });

  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error).toBe('invalid_email');
  }
});

it('handles database errors gracefully', async () => {
  const deps = createMockDependencies({
    insertUser: async () => { throw new Error('Connection timeout'); },
  });

  const result = await createUser(deps, validArgs);

  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error).toBe('database_error');
  }
});

Testing Discriminated Unions

For functions returning discriminated union results:

type CreateUserResult =
  | { readonly success: true; readonly user: User }
  | { readonly success: false; readonly error: 'email_exists' | 'invalid_email' };

it('returns success with user data', async () => {
  const result = await createUser(deps, validArgs);

  // Type narrowing via discriminant
  expect(result.success).toBe(true);
  if (result.success) {
    expect(result.user.email).toBe('test@example.com');
    expect(result.user.id).toBeDefined();
  }
});

it('returns specific error code', async () => {
  const deps = createMockDependencies({
    findUserByEmail: async () => existingUser,
  });

  const result = await createUser(deps, { email: existingUser.email, name: 'New' });

  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error).toBe('email_exists');
  }
});

Coverage Guidelines

What to Cover

  • All public functions
  • All code paths (success, error, edge cases)
  • All acceptance criteria from specs
  • Boundary conditions

What NOT to Cover

  • Private implementation details
  • Framework code
  • Generated types
  • Simple getters/setters

Coverage Targets

MetricTarget
Line coverage≥80%
Branch coverage≥75%
Function coverage≥90%

Rules

  • One test file per source file - create_user.ts → create_user.test.ts
  • Test behavior, not implementation - Focus on inputs/outputs, not internal details
  • Each AC = at least one test - Map tests to acceptance criteria
  • Reference spec and issue - Use @spec and @issue JSDoc tags
  • Given/When/Then structure - Clear Arrange/Act/Assert sections
  • Fast execution - Unit tests should complete in milliseconds
  • No external dependencies - Mock all I/O (DB, HTTP, filesystem)
  • Isolated tests - Tests must not depend on each other
  • Descriptive names - Test names should describe the scenario

Summary Checklist

Before committing unit tests, verify:

  • Test file is next to source file with .test.ts suffix
  • @spec and @issue tags present in file header
  • Each acceptance criterion has corresponding tests
  • Dependencies are mocked (no real I/O)
  • Tests follow Given/When/Then structure
  • Tests are isolated (no shared mutable state)
  • All async operations properly awaited
  • Error scenarios tested
  • Tests run fast (< 100ms per test)

Input / Output

This skill defines no input parameters or structured output.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Standards for authoring SDD plugin agents — frontmatter, self-containment, skill references, and no-user-interaction rules.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Scaffolds Node.js/TypeScript backend components with CMDO architecture, driven by component settings.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

CMDO architecture standards for Node.js/TypeScript backends with strict layer separation.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Intent-to-command mappings for fullstack-typescript tech pack features.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Create change specification and implementation plan with dynamic phase generation. Supports feature, bugfix, refactor, and epic types.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

Orchestrates the full change lifecycle — routes actions to phase-specific sub-files.

日本語の概要は準備中です。原文の説明を表示しています。

LiorCohen/sdd442026年2月24日 更新

LiorCohen のスキルをすべて見る

このスキルの問題を報告する