본문으로 건너뛰기
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.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구Terraform
설치 방법 보기

포함된 파일(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).

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

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의 스킬 모두 보기

이 스킬의 문제 신고