mcpField
T
test-writer

test-writer

github.com/leonardomsoテスト・品質66.5k354 回インストール21 週間前に更新
リポジトリ

test-writer について

test-writerは、JavaScriptの概念ドキュメントにあるコード例からVitestテストを作成するスキルです。例の分類、テストファイルの規約、ソース行の参照を示し、ドキュメント作成者による例の検証を支援します。

主な機能

  1. 01テスト可能性に応じた例の分類
  2. 02Vitestアサーションの作成
  3. 03DOMテストファイルの分離
  4. 04非同期処理とエラーのテストパターン
  5. 05ドキュメントのソース行参照

活用シーン

  1. 01新しい概念ページの例をテスト
  2. 02コード例の変更後にテストを追加
  3. 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:例をテストに変換する

テスト可能なコード例ごとに、次の手順を実行します。

  1. 期待される出力を特定する(console.log のコメント、またはドキュメントで説明されている動作を参照)
  2. expect アサーションに変換する
  3. コメントにソースの行番号を記載する
  4. ドキュメントのセクションに合わせて describe ブロックで関連するテストをまとめる
フェーズ 4:特殊なケースへの対応
ケース解決方法
ブラウザー専用 APIjsdom 環境を使うか、注記を付けてスキップする
タイミングに依存するコード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()
})

まとめ

概念ページのテストを作成する際は、次の手順に従います。

  1. ドキュメントからすべてのコード例を抽出します
  2. 分類します(テスト可能、DOM、エラー、概念)
  3. 適切な場所に正しい名前でテストファイルを作成します
  4. 適切なパターンを使って、各例をテストに変換します
  5. 追跡できるよう、コメントにソース行を記載します
  6. すべて成功することを確認するため、テストを実行します
  7. レポートのテンプレートを使ってカバレッジを記録します

注意: テストには 2 つの目的があります。

  1. ドキュメントが正確であることを検証する
  2. コード例が更新された場合にリグレッションを検出する

ドキュメント内のテスト可能なコード例には、すべて対応するテストが必要です。テストできない例がある場合は、その理由を記載してください。

コメント