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.
日本語の概要は準備中です。原文の説明を表示しています。