mcpField
T
test-writer

test-writer

github.com/leonardomso测试与质量66.5k354 次安装21 周前更新
代码仓库

关于 test-writer

test-writer 是一项技能,用于为 JavaScript 概念文档中的代码示例编写 Vitest 测试。它通过示例分类、测试文件约定和源码行引用,帮助文档作者验证示例。

主要功能

  1. 01按可测试性分类示例
  2. 02生成 Vitest 断言
  3. 03将 DOM 测试单独存放
  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 测试。测试用于验证文档中的代码示例是否准确,并能按描述正常运行。

使用时机

  • 编写新的概念页面后
  • 为现有页面添加新的代码示例时
  • 更新现有代码示例时
  • 通过自动化测试验证文档准确性时
  • 发布前确保所有示例都能正常运行

测试编写方法

遵循以下四个阶段,为概念页面创建全面的测试。

阶段 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:处理特殊情况
情况解决方案
仅限浏览器的 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
  • 对象比较使用 toEqual(而非 toBe)
规范
  • 使用来自 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,并将函数标记为异步

// 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. 使用报告模板记录覆盖情况

请记住: 测试有两个用途:

  1. 验证文档是否准确
  2. 在代码示例更新时发现回归问题

文档中每个可测试的代码示例都应有对应的测试。如果某个示例无法测试,请说明原因。

评论

支持 Markdown