test-writer について
test-writerは、JavaScriptの概念ドキュメントにあるコード例からVitestテストを作成するスキルです。例の分類、テストファイルの規約、ソース行の参照を示し、ドキュメント作成者による例の検証を支援します。
主な機能
- 01テスト可能性に応じた例の分類
- 02Vitestアサーションの作成
- 03DOMテストファイルの分離
- 04非同期処理とエラーのテストパターン
- 05ドキュメントのソース行参照
活用シーン
- 01新しい概念ページの例をテスト
- 02コード例の変更後にテストを追加
- 03公開前にドキュメント例を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ leonardomso/ 33-js-concepts --skill test-writerインストール後、Claude Code で/test-writerを入力して読み込まれたか確認します。
詳細
スキル:コンセプトページのテスト作成
このスキルを使って、コンセプトドキュメントページのすべてのコード例を対象とする、包括的な Vitest テストを作成します。テストでは、ドキュメント内のコード例が正確で、説明どおりに動作することを検証します。
使用するタイミング
- 新しいコンセプトページを作成した後
- 既存のページに新しいコード例を追加するとき
- 既存のコード例を更新するとき
- 自動テストでドキュメントの正確性を検証するとき
- すべてのコード例が正しく動作することを確認してから公開するとき
テスト作成の方法
以下の4つのフェーズに従って、コンセプトページの包括的なテストを作成します。
フェーズ 1:コード例の抽出
コンセプトページ内のすべてのコード例を確認し、次のように分類します。
| カテゴリー | 特徴 | 対応 |
|---|---|---|
| テスト可能 | 出力コメント付きの console.log がある、または値を返す | テストを作成する |
| DOM 固有 | document、window、DOM API、イベントハンドラーを使う | DOM テストを作成する(別ファイル) |
| エラーの例 | 意図的にエラーをスローする、または失敗を示す | toThrow を使ったテストを作成する |
| 概念的 | ASCII 図、疑似コード、不完全なスニペット | スキップする(理由を記録する) |
| ブラウザー専用 | jsdom では利用できないブラウザー API を使う | スキップするか、モックを使う |
フェーズ 2:テストファイルの構成を決める
tests/
├── fundamentals/ # Concepts 1-6
├── functions-execution/ # Concepts 7-8
├── web-platform/ # Concepts 9-10
├── object-oriented/ # Concepts 11-15
├── functional-programming/ # Concepts 16-19
├── async-javascript/ # Concepts 20-22
├── advanced-topics/ # Concepts 23-31
└── beyond/ # Extended concepts
└── {subcategory}/
ファイル名:
- 標準テスト:
{concept-name}.test.js - DOM テスト:
{concept-name}.dom.test.js
フェーズ 3:例をテストに変換する
テスト可能なコード例ごとに、次の手順を実行します。
- 期待される出力を特定する(
console.logのコメント、またはドキュメントで説明されている動作を参照) expectアサーションに変換する- コメントにソースの行番号を記載する
- ドキュメントのセクションに合わせて
describeブロックで関連するテストをまとめる
フェーズ 4:特殊なケースへの対応
| ケース | 解決方法 |
|---|---|
| ブラウザー専用 API | jsdom 環境を使うか、注記を付けてスキップする |
| タイミングに依存するコード | vi.useFakeTimers() を使うか、タイミングではなくロジックをテストする |
| 副作用 | 出力を取得するか、変更をテストする |
| 意図的なエラー | expect(() => {...}).toThrow() を使う |
| 非同期コード | 適切なアサーションとともに async/await を使う |
プロジェクトのテスト規約
インポートパターン
import { describe, it, expect } from 'vitest'
DOM テスト、またはモックが必要なテストの場合:
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
DOM テストファイルのヘッダー
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
Describe ブロックの構成
ドキュメントの構成に合わせます。
describe('Concept Name', () => {
describe('Section from Documentation', () => {
describe('Subsection if needed', () => {
it('should [specific behavior]', () => {
// Test
})
})
})
})
テストの命名規則
- "should" で始める
- 説明的かつ具体的にする
- ドキュメントで説明されている動作に合わせる
// Good
it('should return "object" for typeof null', () => {})
it('should throw TypeError when accessing property of undefined', () => {})
it('should resolve promises in order they were created', () => {})
// Bad
it('test typeof', () => {})
it('works correctly', () => {})
it('null test', () => {})
ソースの行番号
ドキュメント内のソースを必ず参照します。
// ============================================================
// SECTION NAME FROM DOCUMENTATION
// From {concept}.mdx lines XX-YY
// ============================================================
describe('Section Name', () => {
// From lines 45-52: Basic typeof examples
it('should return correct type strings', () => {
// Test
})
})
テストパターンのリファレンス
パターン 1:基本的な値のアサーション
ドキュメント:
console.log(typeof "hello") // "string"
console.log(typeof 42) // "number"
テスト:
// From lines XX-YY: typeof examples
it('should return correct type for primitives', () => {
expect(typeof "hello").toBe("string")
expect(typeof 42).toBe("number")
})
パターン 2:関連する複数のアサーション
ドキュメント:
let a = "hello"
let b = "hello"
console.log(a === b) // true
let obj1 = { x: 1 }
let obj2 = { x: 1 }
console.log(obj1 === obj2) // false
テスト:
// From lines XX-YY: Primitive vs object comparison
it('should compare primitives by value', () => {
let a = "hello"
let b = "hello"
expect(a === b).toBe(true)
})
it('should compare objects by reference', () => {
let obj1 = { x: 1 }
let obj2 = { x: 1 }
expect(obj1 === obj2).toBe(false)
})
パターン 3:関数の戻り値
ドキュメント:
function greet(name) {
return "Hello, " + name + "!"
}
console.log(greet("Alice")) // "Hello, Alice!"
テスト:
// From lines XX-YY: greet function example
it('should return greeting with name', () => {
function greet(name) {
return "Hello, " + name + "!"
}
expect(greet("Alice")).toBe("Hello, Alice!")
})
パターン 4:エラーのテスト
ドキュメント:
// This throws an error!
const obj = null
console.log(obj.property) // TypeError: Cannot read property of null
テスト:
// From lines XX-YY: Accessing property of null
it('should throw TypeError when accessing property of null', () => {
const obj = null
expect(() => {
obj.property
}).toThrow(TypeError)
})
パターン 5:特定のエラーメッセージ
ドキュメント:
function divide(a, b) {
if (b === 0) throw new Error("Cannot divide by zero")
return a / b
}
テスト:
// From lines XX-YY: divide function with error
it('should throw error when dividing by zero', () => {
function divide(a, b) {
if (b === 0) throw new Error("Cannot divide by zero")
return a / b
}
expect(() => divide(10, 0)).toThrow("Cannot divide by zero")
expect(divide(10, 2)).toBe(5)
})
パターン 6:Async/Await のテスト
ドキュメント:
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
テスト:
// From lines XX-YY: async fetchUser function
it('should fetch user data asynchronously', async () => {
// Mock fetch for testing
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ id: 1, name: 'Alice' })
})
)
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
const user = await fetchUser(1)
expect(user).toEqual({ id: 1, name: 'Alice' })
})
パターン 7:Promise のテスト
ドキュメント:
const promise = new Promise((resolve) => {
resolve("done")
})
promise.then(result => console.log(result)) // "done"
テスト:
// From lines XX-YY: Basic Promise resolution
it('should resolve with correct value', async () => {
const promise = new Promise((resolve) => {
resolve("done")
})
await expect(promise).resolves.toBe("done")
})
パターン 8:Promise の拒否
ドキュメント:
const promise = new Promise((resolve, reject) => {
reject(new Error("Something went wrong"))
})
テスト:
// From lines XX-YY: Promise rejection
it('should reject with error', async () => {
const promise = new Promise((resolve, reject) => {
reject(new Error("Something went wrong"))
})
await expect(promise).rejects.toThrow("Something went wrong")
})
パターン 9:浮動小数点数の比較
ドキュメント:
console.log(0.1 + 0.2) // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3) // false
テスト:
// From lines XX-YY: Floating point precision
it('should demonstrate floating point imprecision', () => {
expect(0.1 + 0.2).not.toBe(0.3)
expect(0.1 + 0.2).toBeCloseTo(0.3)
expect(0.1 + 0.2 === 0.3).toBe(false)
})
パターン 10:配列メソッドのテスト
ドキュメント:
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map(n => n * 2)
console.log(doubled) // [2, 4, 6, 8, 10]
テスト:
// From lines XX-YY: Array map example
it('should double all numbers in array', () => {
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map(n => n * 2)
expect(doubled).toEqual([2, 4, 6, 8, 10])
expect(numbers).toEqual([1, 2, 3, 4, 5]) // Original unchanged
})
パターン 11:オブジェクトの変更のテスト
ドキュメント:
const obj = { a: 1 }
obj.b = 2
console.log(obj) // { a: 1, b: 2 }
テスト:
// From lines XX-YY: Object mutation
it('should allow adding properties to objects', () => {
const obj = { a: 1 }
obj.b = 2
expect(obj).toEqual({ a: 1, b: 2 })
})
パターン 12:クロージャーのテスト
ドキュメント:
function counter() {
let count = 0
return function() {
count++
return count
}
}
const increment = counter()
console.log(increment()) // 1
console.log(increment()) // 2
console.log(increment()) // 3
テスト:
// From lines XX-YY: Closure counter example
it('should maintain state across calls via closure', () => {
function counter() {
let count = 0
return function() {
count++
return count
}
}
const increment = counter()
expect(increment()).toBe(1)
expect(increment()).toBe(2)
expect(increment()).toBe(3)
})
it('should create independent counters', () => {
function counter() {
let count = 0
return function() {
count++
return count
}
}
const counter1 = counter()
const counter2 = counter()
expect(counter1()).toBe(1)
expect(counter1()).toBe(2)
expect(counter2()).toBe(1) // Independent
})
パターン 13:DOM イベントのテスト
ドキュメント:
const button = document.getElementById('myButton')
button.addEventListener('click', function(event) {
console.log('Button clicked!')
console.log(event.type) // "click"
})
テスト(.dom.test.js ファイル内):
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
describe('DOM Event Handlers', () => {
let button
beforeEach(() => {
button = document.createElement('button')
button.id = 'myButton'
document.body.appendChild(button)
})
afterEach(() => {
document.body.innerHTML = ''
})
// From lines XX-YY: Button click event
it('should fire click event handler', () => {
const output = []
button.addEventListener('click', function(event) {
output.push('Button clicked!')
output.push(event.type)
})
button.click()
expect(output).toEqual(['Button clicked!', 'click'])
})
})
パターン 14:DOM 操作のテスト
ドキュメント:
const div = document.createElement('div')
div.textContent = 'Hello'
div.classList.add('greeting')
document.body.appendChild(div)
テスト:
// From lines XX-YY: Creating and appending elements
it('should create element with text and class', () => {
const div = document.createElement('div')
div.textContent = 'Hello'
div.classList.add('greeting')
document.body.appendChild(div)
const element = document.querySelector('.greeting')
expect(element).not.toBeNull()
expect(element.textContent).toBe('Hello')
expect(element.classList.contains('greeting')).toBe(true)
})
パターン 15:タイマーのテスト
ドキュメント:
console.log('First')
setTimeout(() => console.log('Second'), 0)
console.log('Third')
// Output: First, Third, Second
テスト:
// From lines XX-YY: setTimeout execution order
it('should execute setTimeout callback after synchronous code', async () => {
const output = []
output.push('First')
setTimeout(() => output.push('Second'), 0)
output.push('Third')
// Wait for setTimeout to execute
await new Promise(resolve => setTimeout(resolve, 10))
expect(output).toEqual(['First', 'Third', 'Second'])
})
パターン 16:厳格モードの動作
ドキュメント:
// In strict mode, this throws
"use strict"
x = 10 // ReferenceError: x is not defined
テスト:
// From lines XX-YY: Strict mode variable declaration
it('should throw ReferenceError in strict mode for undeclared variables', () => {
// Vitest runs in strict mode by default
expect(() => {
// Using eval to test strict mode behavior
"use strict"
eval('undeclaredVar = 10')
}).toThrow()
})
完全なテストファイルのテンプレート
import { describe, it, expect } from 'vitest'
describe('[Concept Name]', () => {
// ============================================================
// [FIRST SECTION NAME FROM DOCUMENTATION]
// From [concept].mdx lines XX-YY
// ============================================================
describe('[First Section]', () => {
// From lines XX-YY: [Brief description of example]
it('should [expected behavior]', () => {
// Code from documentation
expect(result).toBe(expected)
})
// From lines XX-YY: [Brief description of next example]
it('should [another expected behavior]', () => {
// Code from documentation
expect(result).toEqual(expected)
})
})
// ============================================================
// [SECOND SECTION NAME FROM DOCUMENTATION]
// From [concept].mdx lines XX-YY
// ============================================================
describe('[Second Section]', () => {
// From lines XX-YY: [Description]
it('should [behavior]', () => {
// Test
})
})
// ============================================================
// EDGE CASES AND COMMON MISTAKES
// From [concept].mdx lines XX-YY
// ============================================================
describe('Edge Cases', () => {
// From lines XX-YY: [Edge case description]
it('should handle [edge case]', () => {
// Test
})
})
describe('Common Mistakes', () => {
// From lines XX-YY: Wrong way example
it('should demonstrate the incorrect behavior', () => {
// Test showing why the "wrong" way fails
})
// From lines XX-YY: Correct way example
it('should demonstrate the correct behavior', () => {
// Test showing the right approach
})
})
})
完全な DOM テストファイルのテンプレート
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// ============================================================
// DOM EXAMPLES FROM [CONCEPT NAME]
// From [concept].mdx lines XX-YY
// ============================================================
describe('[Concept Name] - DOM', () => {
// Shared setup
let container
beforeEach(() => {
// Create a fresh container for each test
container = document.createElement('div')
container.id = 'test-container'
document.body.appendChild(container)
})
afterEach(() => {
// Clean up after each test
document.body.innerHTML = ''
vi.restoreAllMocks()
})
// ============================================================
// [SECTION NAME]
// From lines XX-YY
// ============================================================
describe('[Section Name]', () => {
// From lines XX-YY: [Example description]
it('should [expected DOM behavior]', () => {
// Setup
const element = document.createElement('div')
container.appendChild(element)
// Action
element.textContent = 'Hello'
// Assert
expect(element.textContent).toBe('Hello')
})
})
// ============================================================
// EVENT HANDLING
// From lines XX-YY
// ============================================================
describe('Event Handling', () => {
// From lines XX-YY: Click event example
it('should handle click events', () => {
const button = document.createElement('button')
container.appendChild(button)
let clicked = false
button.addEventListener('click', () => {
clicked = true
})
button.click()
expect(clicked).toBe(true)
})
})
})
テストの実行
# Run all tests
npm test
# Run tests for specific concept
npm test -- tests/fundamentals/primitive-types/
# Run tests for specific file
npm test -- tests/fundamentals/primitive-types/primitive-types.test.js
# Run DOM tests only
npm test -- tests/fundamentals/primitive-types/primitive-types.dom.test.js
# Run with watch mode
npm run test:watch
# Run with coverage
npm run test:coverage
# Run with verbose output
npm test -- --reporter=verbose
品質チェックリスト
網羅性
- テスト可能なコード例すべてに対応するテストがある
- テストがドキュメントのセクションごとに整理されている
- コメントにソース行への参照が含まれている(From lines XX-YY)
- DOM テストが別の
.dom.test.jsファイルにある - エッジケースとエラーの例がテストされている
正確性
- テストで、実際にドキュメントに記載された動作を検証している
- ドキュメント内の出力コメントがテストの期待値と一致している
- 非同期テストで async/await を適切に使用している
- エラーテストで正しい
toThrowパターンを使用している - 浮動小数点数の比較に
toBeCloseToを使用している - オブジェクトの比較に
toBeではなくtoEqualを使用している
規約
- vitest から明示的にインポートしている
- describe/it のネストパターンに従っている
- テスト名が「should」で始まっている
- 適切なファイル名を使用している(
{concept}.test.js) - DOM テストに jsdom 環境ディレクティブがある
検証
- すべてのテストが成功する:
npm test -- tests/{category}/{concept}/ - 理由を記載せずにスキップしたテストがない
- 誤検出(誤った理由で成功するテスト)がない
テストレポートのテンプレート
このテンプレートを使用して、概念ページのテストカバレッジを記録します。
# Test Coverage Report: [Concept Name]
**Concept Page:** `/docs/concepts/[slug].mdx`
**Test File:** `/tests/{category}/{concept}/{concept}.test.js`
**DOM Test File:** `/tests/{category}/{concept}/{concept}.dom.test.js` (if applicable)
**Date:** YYYY-MM-DD
**Author:** [Name/Claude]
## Summary
| Metric | Count |
|--------|-------|
| Total Code Examples in Doc | XX |
| Testable Examples | XX |
| Tests Written | XX |
| DOM Tests Written | XX |
| Skipped (with reason) | XX |
## Tests by Section
| Section | Line Range | Examples | Tests | Status |
|---------|------------|----------|-------|--------|
| [Section 1] | XX-YY | X | X | ✅ |
| [Section 2] | XX-YY | X | X | ✅ |
| [Section 3] | XX-YY | X | X | ⚠️ (1 skipped) |
## Skipped Examples
| Line | Example Description | Reason |
|------|---------------------|--------|
| XX | ASCII diagram of call stack | Conceptual, not executable |
| YY | Browser fetch example | Requires network, mocked instead |
## Test Execution
```bash
npm test -- tests/{category}/{concept}/
結果: ✅ XX 件成功 | ❌ X 件失敗 | ⏭️ X 件スキップ
備考
[特別な考慮事項、モックの要件、または発生した問題]
---
## Common Issues and Solutions
### Issue: Test passes but shouldn't
**Problem:** Test expectations don't match documentation output
**Solution:** Double-check the expected value matches the `console.log` comment exactly
```javascript
// Documentation says: console.log(result) // [1, 2, 3]
// Make sure test uses:
expect(result).toEqual([1, 2, 3]) // NOT toBe for arrays
問題:非同期テストがタイムアウトする
問題: 非同期テストが解決されない
解決策: すべての Promise が await されていること、および非同期関数としてマークされていることを確認します
// Bad
it('should fetch data', () => {
const data = fetchData() // Missing await!
expect(data).toBeDefined()
})
// Good
it('should fetch data', async () => {
const data = await fetchData()
expect(data).toBeDefined()
})
問題:DOM テストで「document is not defined」エラーが発生する
問題: jsdom 環境が設定されていない
解決策: ファイルの先頭に環境ディレクティブを追加します
/**
* @vitest-environment jsdom
*/
問題:テストの分離に関する問題
問題: テスト同士が影響し合う
解決策: クリーンアップには beforeEach/afterEach を使用します
afterEach(() => {
document.body.innerHTML = ''
vi.restoreAllMocks()
})
まとめ
概念ページのテストを作成する際は、次の手順に従います。
- ドキュメントからすべてのコード例を抽出します
- 分類します(テスト可能、DOM、エラー、概念)
- 適切な場所に正しい名前でテストファイルを作成します
- 適切なパターンを使って、各例をテストに変換します
- 追跡できるよう、コメントにソース行を記載します
- すべて成功することを確認するため、テストを実行します
- レポートのテンプレートを使ってカバレッジを記録します
注意: テストには 2 つの目的があります。
- ドキュメントが正確であることを検証する
- コード例が更新された場合にリグレッションを検出する
ドキュメント内のテスト可能なコード例には、すべて対応するテストが必要です。テストできない例がある場合は、その理由を記載してください。