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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
Establish reusable patterns for writing comprehensive test suites with Vitest in TypeScript ESM projects.
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.
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,
})),
}
})
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)
})
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 })
})
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')
})
For testing multi-step orchestrators that coordinate multiple subsystems:
beforeEach with vi.clearAllMocks() for isolationit('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')
})
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:
ResourceChange[], ProviderPlan) into the new functionstep.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)
})
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.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.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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.