这个 Skill 解决什么具体问题

每次写单元测试都像在重复劳动——分析代码、构思用例、手写断言。更烦的是,不同项目风格不同,团队标准总得时刻记在脑子里。

你需要的是一个可复用的 AI 技能:把“写测试”这件事拆成稳定步骤,封装成一份 SKILL.md,让 agent 每次执行时输出一样质量的结果。

Superpowers 项目的核心就是这个思路:用结构化描述定义 agent 的能力边界和工作流程,不再靠零散的 prompt 碰运气。


触发条件和适用场景

  • 触发条件:你提交了一个 .py 文件,或者手动调用 @skill test-gen
  • 适用场景
    • 新功能开发后需要测试覆盖
    • 重构前需要保住现有行为
    • 代码审查时快速补测试

不适用的场景:UI 测试(需要浏览器驱动)、大型遗留系统(上下文超出 token 限制)。


完整 Skill 结构(SKILL.md 示例)

Superpowers 的方法论鼓励把技能写成 SKILL.md,包含 triggerrolestepsoutput schema。下面是让你直接复用的模板:

markdown
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# Skill: test-gen

## Trigger
当用户要求“生成测试”或提交代码时触发。

## Role
你是一个资深测试工程师,熟悉 pytest,遵循 Given-When-Then 风格。

## Steps
1. 读取目标文件,识别所有函数和类方法。
2. 对每个公共函数:
   - 确定输入参数类型和返回值类型
   - 列出至少 3 个边界值
   - 写一个参数化测试用例(pytest.mark.parametrize)
3. 对类方法:模拟外部依赖(mock),只测业务逻辑。
4. 输出为 `test_<module>.py`,遵循项目现有的 conftest 配置。

## Output Schema
- 文件名:`test_<original_file>`
- 文件内容:完整的 pytest 代码,包含必要的 import

为什么这样写有效? 关键是 Role + Steps + Output Schema 三位一体。Role 限定行为边界,Steps 分解原子操作,Output Schema 固定结果格式。差 Prompt 经常缺一,导致输出不稳定。


差 Prompt vs 好 Prompt

差 Prompt(直接问):

text
1 2 3
给下面代码写测试:
def add(a, b):
    return a + b

输出结果可能是单边断言、缺少 import、或者连参数类型都不考虑。

好 Prompt(基于 SKILL.md 的触发指令):

text
1 2 3 4 5 6
@skill test-gen
目标代码:
def divide(a, b):
    if b == 0:
        raise ValueError("b cannot be zero")
    return a / b

输出的完整测试:

python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import pytest
from my_module import divide

@pytest.mark.parametrize("a,b,expected_exception", [
    (10, 2, None),   # 正常
    (0, 5, None),    # 零被除数
    (5, 0, ValueError),  # 除数为零
    (-1, 2, None),   # 负数
])
def test_divide(a, b, expected_exception):
    if expected_exception:
        with pytest.raises(expected_exception):
            divide(a, b)
    else:
        assert divide(a, b) == a / b

对比效果:差 prompt 几乎不会主动列边界值,好 prompt 因为 Steps 里写了“至少 3 个边界值”,agent 自然会覆盖零值、负数、异常。这就是结构化流程的力量。


实际案例演示

假设你有一个 user_service.py,包含一个注册方法:

python
1 2 3 4 5 6 7 8
class UserService:
    def register(self, email, password):
        if len(password) < 8:
            raise ValueError("Password too short")
        if "@" not in email:
            raise ValueError("Invalid email")
        # ...save to DB...
        return User(email)

触发 @skill test-gen 后,agent 依据 SKILL.md 的步骤输出:

python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import pytest
from unittest.mock import MagicMock, patch
from user_service import UserService

class TestUserService:
    @pytest.mark.parametrize("email,password,expected", [
        ("test@example.com", "pass1234", None),  # 正常
        ("test@example.com", "short", ValueError),  # 密码太短
        ("noatsign.com", "password123", ValueError),  # 邮箱无效
        ("", "password123", ValueError),  # 空邮箱
    ])
    def test_register(self, email, password, expected):
        svc = UserService()
        with patch.object(svc, '_save') as mock_save:
            if expected:
                with pytest.raises(expected):
                    svc.register(email, password)
                mock_save.assert_not_called()
            else:
                result = svc.register(email, password)
                assert result.email == email
                mock_save.assert_called_once()

注意里的 patch.object(svc, '_save') —— 这来自 Steps 中“模拟外部依赖”的指令。差 prompt 基本不会主动 mock 数据库。


复用和组合技巧

  1. 绑定到 Git Hook:在 pre-push 钩子中调用 @skill test-gen,对变更文件自动生成测试。
  2. 组合技能:先 @skill code-review 审查代码,再 @skill test-gen 生成测试,最后 @skill lint-fix 格式化。把三个 SKILL.md 用 workflow 串联。
  3. 微调输出风格:在 SKILL.md 的 Output Schema 中增加 ## Code Style 段落,指定团队约定(如 pytest 使用 plain assert 还是 unittest)。
  4. 变体用法:将 SKILL.md 中的 Role 改为“初级开发”,Steps 减少到只生成 happy path 测试,用于快速覆盖率摸底。
yaml
1 2 3 4 5 6
# .superworks/skills/test-gen-lite/skill.yaml
trigger: "@skill test-lite"
role: 初级开发
steps:
  - 对每个函数写一个最简测试(仅正常路径)
  - 只使用 assert 单值,不用 parametrize

这个轻量版可以配合 CI 的 pytest --cov 快速提升代码覆盖率,减少阻塞。


一句话收尾:别每次都从头写 prompt,把重复逻辑封装进 SKILL.md,让 AI 变成你的稳定外包队员。