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

vitest-test-patterns

Patterns for writing comprehensive Vitest test suites in TypeScript projects. Covers shared test utilities, vi.mock for module mocking, fixture directories, provider/adapter testing, and state management testing. Tier - generic.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md7.8 KB

SKILL.md(原文)

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

Vitest Test Patterns

Goal

Establish reusable patterns for writing comprehensive test suites with Vitest in TypeScript ESM projects.

Shared Test Utilities

Create src/__test-utils__/ with reusable helpers:

// __test-utils__/mock-adapter.ts
import { vi } from 'vitest'
import type { IAdapter } from '../adapters/adapter.js'

export class MockAdapter implements IAdapter {
  // Use vi.fn() for every method — enables spying, return value control, call assertions
  doWork = vi.fn<IAdapter['doWork']>((): Promise<Result> => {
    throw new Error('Method not implemented.')
  })
  getName = vi.fn(() => 'mock')
}

// Factory helpers with sensible defaults
export function createThing(name: string, overrides: Partial<ThingConfig> = {}): Thing {
  return new Thing(name, `/path/to/${name}`, { env: 'dev', ...overrides })
}

Key pattern: Use vi.fn<InterfaceType['methodName']>() for type-safe mock methods.

Module Mocking with vi.mock

Mocking a dependency with shared state

When the SUT creates its own instance of a dependency internally, mock the module and use shared variables to control behavior:

const mockRead = vi.fn()
const mockUpdate = vi.fn()

vi.mock('./state-manager.js', async (importOriginal) => {
  const original = await importOriginal<typeof import('./state-manager.js')>()
  return {
    ...original, // Preserve non-mocked exports (e.g., State class)
    StateManager: vi.fn().mockImplementation(() => ({
      read: mockRead,
      update: mockUpdate,
    })),
  }
})

Gotcha: Shared state across mock calls

When mocking update(fn) where fn mutates state, use a shared state object so mutations from one call are visible to the next:

// WRONG — each call gets a fresh object, mutations are lost
mockUpdate.mockImplementation(async (fn) => {
  fn(new State()) // ← startSelectingEnv() lost before finishEnvSelection()
})

// RIGHT — shared state preserves mutations across calls
let sharedState = new State()
mockUpdate.mockImplementation(async (fn) => {
  fn(sharedState)
})

Static Fixture Directories

For filesystem-dependent code (config parsers, file scanners), use static fixture dirs checked into the repo:

src/core/__fixtures__/
  simple-case/         # Happy path
    config.yaml
    sub-dir/config.yaml
  edge-case/           # Error paths, missing files
    config.yaml
  complex-case/        # Multiple interacting components
    config.yaml
    a/config.yaml
    b/config.yaml

Reference fixtures with import.meta.dirname:

const FIXTURES_DIR = resolve(import.meta.dirname, '__fixtures__')

it('should parse config', async () => {
  const result = await parseConfig(join(FIXTURES_DIR, 'simple-case'))
  expect(result.items).toHaveLength(2)
})

When to use temp dirs instead: For tests that write files (state persistence, temp files). Use mkdtemp + afterEach cleanup:

let tmpDir: string
beforeEach(async () => {
  tmpDir = await mkdtemp(join(tmpdir(), 'test-'))
})
afterEach(async () => {
  await rm(tmpDir, { recursive: true })
})

Extract Pure Functions for Testability

When a class has complex private parsing/transformation logic, extract it as an exported pure function while keeping the class method as a one-line delegate:

// Before: private method, untestable directly
class MyProvider {
  private parseOutput(raw: string): Plan {
    /* 80 lines of parsing */
  }
}

// After: exported pure function + thin delegate
class MyProvider {
  private parseOutput(raw: string): Plan {
    return parseProviderOutput(raw, this.name)
  }
}

export function parseProviderOutput(raw: string, name: string): Plan {
  /* same 80 lines, now directly testable */
}

Test the exported function with fixture data:

// __test-utils__/provider-fixtures.ts — raw JSON strings representing real CLI output
export const PROVIDER_OUTPUT_CREATE = '{"type":"create","resources":[...]}'
export const PROVIDER_OUTPUT_NO_CHANGES = '{"type":"no-op"}'

// provider.test.ts
import { parseProviderOutput } from './provider.js'
import { PROVIDER_OUTPUT_CREATE } from '../__test-utils__/provider-fixtures.js'

it('should parse create output', () => {
  const plan = parseProviderOutput(PROVIDER_OUTPUT_CREATE, 'my-project')
  expect(plan.changes).toHaveLength(2)
  expect(plan.changes[0].action).toBe('create')
})

Orchestration/Integration-Style Tests

For testing multi-step orchestrators that coordinate multiple subsystems:

  1. Mock all dependencies at the module level
  2. Use beforeEach with vi.clearAllMocks() for isolation
  3. Set up preconditions with mock return values per test
  4. Assert the interaction sequence: calls made, call order, arguments passed
it('should execute level 1 then level 2', async () => {
  mockGetPlan.mockResolvedValue(planWithChanges)
  let callOrder = 0
  mockApply.mockImplementation(async () => {
    callOrder++
    return callOrder === 1
      ? { network: 'dev_net' } // level 1 output
      : { db: 'localhost:5432' } // level 2 output
  })

  await executor.exec(opts)

  expect(mockApply).toHaveBeenCalledTimes(2)
  expect(ctx.findOutput('network', 'network')).toBe('dev_net')
})

E2E Tests for Provider-Dependent Logic

When new functionality processes or depends on real provider output (e.g., Terraform plan JSON, Pulumi preview JSON), always add e2e tests (*.e2e.test.ts) that:

  1. Parse raw CLI output from fixture strings through the real provider parser
  2. Feed the resulting typed structures (e.g., ResourceChange[], ProviderPlan) into the new function
  3. Assert correct behavior against both provider formats (they differ structurally — e.g., Pulumi uses step.oldState.inputs/step.newState.inputs, Terraform uses inline before/after)

This catches integration issues that unit tests with hand-crafted objects miss (unexpected field shapes, nested structures, null patterns).

// provider-fixtures.ts — add new fixtures for each scenario
export const PROVIDER_OUTPUT_METADATA_ONLY = [
  '{"type":"planned_change","change":{"action":"update","before":{"x":"same"},"after":{"x":"same"}}}',
  '{"type":"change_summary","changes":{"add":0,"change":1,"remove":0}}',
].join('\n')

// my-feature.e2e.test.ts
import { parseProviderOutput } from '../providers/provider.js'
import { myNewFunction } from './my-feature.js'
import { PROVIDER_OUTPUT_REAL_CHANGE, PROVIDER_OUTPUT_METADATA_ONLY } from '../__test-utils__/provider-fixtures.js'

it('should work with real provider output', () => {
  const plan = parseProviderOutput(PROVIDER_OUTPUT_REAL_CHANGE, 'proj')
  const result = myNewFunction(plan.resourceChanges)
  expect(result.count).toBe(1)
})

Gotchas

  • Zod schema coercion vs raw YAML: If code validates with schema.safeParse(raw) but then uses raw as Type instead of parsed.data, the coercion transforms (e.g., z.coerce.string()) are NOT applied. Test against actual behavior, not schema definition.
  • Module-level singletons (mutex, registry): These persist across tests within a file. Concurrent tests that lock a mutex may deadlock. Prefer sequential operations in tests or mock the singleton.
  • vi.mock hoisting: vi.mock() calls are hoisted to the top of the file. Variables referenced inside the factory must be declared with vi.fn() at module scope — they can't reference let variables from beforeEach.
  • Fixture accuracy: Always verify test fixtures match the real CLI output format — don't trust existing fixtures blindly. Simplified/outdated fixtures can mask parser bugs (e.g., a fixture using step.resource.properties when the real CLI outputs step.oldState.inputs).

レビュー

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

同じリポジトリのスキル

概要と使いどころ

Add --json flag to CLI commands for machine-readable output on stdout. Generic pattern for any CLI tool, enabling piping to jq and CI/CD integration while keeping human-readable output on stderr.

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

krafteq/infraglue22026年7月8日 更新

Add shell completion support via a 'completion' subcommand for bash, zsh, and fish. Generic pattern for any Commander.js CLI, with static and dynamic completion examples.

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

krafteq/infraglue22026年7月8日 更新

Add zod schema validation for CLI options and YAML/JSON config files. Generic pattern for runtime validation with type-safe parsing and clear error messages in any Commander.js CLI.

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

krafteq/infraglue22026年7月8日 更新

Set up @changesets/cli for semantic versioning, CHANGELOG generation, and npm publishing in a monorepo or single package. Covers init, config, contributor workflow, GitHub Actions release automation, and npm provenance.

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

krafteq/infraglue22026年7月8日 更新

Add automatic CI/CD environment detection using ci-info package. Generic pattern for any CLI tool that needs to adapt behavior (prompts, output, integrations) based on whether it's running in CI.

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

krafteq/infraglue22026年7月8日 更新

Polish CLI user experience with @clack/prompts for interactive flows, update-notifier for version awareness, and enhanced Commander.js help text with examples and colors. Generic patterns for any Node.js CLI.

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

krafteq/infraglue22026年7月8日 更新

krafteq のスキルをすべて見る

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