关于 test-specialist
面向 JavaScript/TypeScript、Python、Go 和 Rust 项目的测试编写技能,适用于用户明确要求测试时。它提供单元、集成、端到端、安全和性能测试模式,并包含缺陷分析流程。
主要功能
- 01涵盖 JS/TS、Python、Go、Rust
- 02单元、集成与端到端测试模式
- 03组件测试与行为验证
- 04安全和性能测试示例
- 05结构化缺陷分析流程
适用场景
- 01按请求为功能编写测试
- 02补充关键路径测试覆盖
- 03编写安全或性能测试
- 04通过复现和隔离分析缺陷
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ travisjneuman/ .claude --skill test-specialist安装后在 Claude Code 中输入/test-specialist确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
index.jspackage.jsonreferences/bug_analysis.mdtesting_patterns.mdscripts/analyze_coverage.pyfind_untested_code.py详细介绍
测试专家
适用于 JS/TS、Python、Go 和 Rust 的测试编写模式,以及在用户要求编写测试时适用的 Bug 分析习惯。
使用时机
仅当用户明确要求编写测试时,才使用此技能。若用户没有提出此要求,请通过审查差异及其涉及的代码来验证。
适用于(用户要求编写测试时):
- 编写单元测试、集成测试或端到端测试
- 提高测试覆盖率
- 安全和性能测试
不适用于:
- 未要求编写测试的调试 → 使用
debug-systematic - 代码审查 → 使用
generic-code-reviewer - 技术债务 → 使用
tech-debt-analyzer - 功能开发 → 使用
generic-feature-developer
按项目类型划分的测试栈
| 项目类型 | 单元测试 | 组件 | 端到端 |
|---|---|---|---|
| React/Next.js | Vitest/Jest | Testing Library | Playwright |
| Node.js | Vitest/Jest | Supertest | Playwright |
| 静态项目 | Jest | - | Playwright |
浏览器自动化(Playwright、Percy、Chromatic、npx 浏览器安装)不会在项目所有者的 Mac 上安装或运行。只有在用户提出要求时,才通过用户批准的环境运行。
测试模式
单元测试(AAA 模式)
describe("calculateTotal", () => {
test("sums amounts correctly", () => {
// Arrange
const items = [{ amount: 100 }, { amount: 50 }];
// Act
const total = calculateTotal(items);
// Assert
expect(total).toBe(150);
});
test("handles empty list", () => {
expect(calculateTotal([])).toBe(0);
});
});
组件测试(用户行为)
// ✅ Test user behavior, not implementation
it('creates item when user clicks Add', async () => {
const user = userEvent.setup();
render(<ItemList />);
await user.click(screen.getByRole('button', { name: /add/i }));
await user.type(screen.getByLabelText(/title/i), 'New item');
await user.click(screen.getByRole('button', { name: /save/i }));
expect(screen.getByText('New item')).toBeInTheDocument();
});
端到端测试(Playwright)
import { test, expect } from "@playwright/test";
test("user can complete checkout", async ({ page }) => {
await page.goto("/products");
// Add to cart
await page.click('button:has-text("Add to Cart")');
await page.click('a:has-text("Cart")');
// Checkout
await page.click('button:has-text("Checkout")');
await page.fill('[name="email"]', "[email protected]");
await page.click('button:has-text("Place Order")');
// Verify
await expect(page.locator("h1")).toContainText("Order Confirmed");
});
集成测试
test("POST /items creates item", async () => {
const response = await request(app)
.post("/api/items")
.send({ name: "Test" })
.expect(201);
expect(response.body).toMatchObject({ id: expect.any(Number) });
});
Bug 分析流程
- 复现 - 记录准确步骤、预期结果与实际结果
- 隔离 - 二分查找,创建最小复现
- 查明根因 - 跟踪执行过程、检查假设、使用 git blame
- 修复 - 修复根因(仅在用户要求编写测试时,才先编写失败测试)
- 验证 - 根据复现步骤和边界情况审查差异及其涉及的代码路径
调试检查清单
调试问题时:
- 能否稳定复现
- 是否已创建最小复现
- 是否已检查控制台和网络日志
- 是否已检查失败位置的状态
- 是否已通过 Git blame 检查近期更改
- 是否已根据复现步骤审查修复(仅在用户要求编写测试时,才编写失败测试)
常见 Bug 模式
竞态条件
test("handles concurrent updates", async () => {
const promises = Array.from({ length: 100 }, () => increment());
await Promise.all(promises);
expect(getCount()).toBe(100);
});
空值安全
test.each([null, undefined, "", 0])("handles invalid input: %p", (input) => {
expect(() => process(input)).toThrow("Invalid");
});
边界值
test("handles edge cases", () => {
expect(paginate([], 1, 10)).toEqual([]); // empty
expect(paginate([item], 1, 10)).toEqual([item]); // single
expect(paginate(items25, 3, 10)).toHaveLength(5); // partial last page
});
安全测试
test("prevents SQL injection", async () => {
const malicious = "'; DROP TABLE users; --";
await expect(search(malicious)).resolves.not.toThrow();
});
test("sanitizes XSS", () => {
const xss = '<script>alert("xss")</script>';
expect(sanitize(xss)).not.toContain("<script>");
});
test("requires auth", async () => {
await request(app).post("/api/items").expect(401);
});
性能测试
test("handles large datasets efficiently", () => {
const largeList = Array.from({ length: 10000 }, (_, i) => ({ value: i }));
const start = performance.now();
process(largeList);
expect(performance.now() - start).toBeLessThan(100);
});
覆盖率目标
| 代码类型 | 目标 |
|---|---|
| 关键路径 | 90%+ |
| 业务逻辑 | 85%+ |
| UI 组件 | 75%+ |
| 工具函数 | 70%+ |
测试质量原则
- 每个测试验证一种行为
- 名称清晰 - 测试名称应说明场景
- 测试相互独立 - 不共享状态
- 覆盖边界情况 - 空值、空内容、边界和错误
- 模拟外部依赖 - 测试应快速运行
- 测试行为 - 而非实现细节
工作流决策树
| 情况 | 操作 |
|---|---|
| 添加功能(用户要求编写测试) | 先编写测试(TDD) |
| 修复 Bug(用户要求编写测试) | 先编写失败测试,再修复 |
| 提高覆盖率 | 找出缺口,优先处理关键路径 |
| 代码审查 | 检查边界情况和错误处理 |
Python 测试(pytest)
Fixtures 和参数化
import pytest
from myapp.services import UserService
@pytest.fixture
def user_service(db_session):
"""Provide a UserService with test database."""
return UserService(session=db_session)
@pytest.fixture
def sample_user(user_service):
"""Create and return a sample user."""
return user_service.create(name="Test User", email="[email protected]")
class TestUserService:
def test_create_user(self, user_service):
user = user_service.create(name="John", email="[email protected]")
assert user.name == "John"
assert user.id is not None
def test_get_user_not_found(self, user_service):
with pytest.raises(UserNotFoundError, match="User 999 not found"):
user_service.get(999)
@pytest.mark.parametrize("email,is_valid", [
("[email protected]", True),
("[email protected]", True),
("invalid", False),
("@example.com", False),
("", False),
])
def test_validate_email(self, user_service, email: str, is_valid: bool):
assert user_service.validate_email(email) == is_valid
模拟
from unittest.mock import Mock, patch, AsyncMock
def test_send_notification(user_service):
with patch("myapp.services.email_client") as mock_email:
mock_email.send = Mock(return_value=True)
user_service.notify(user_id=1, message="Hello")
mock_email.send.assert_called_once_with(
to="[email protected]",
body="Hello",
)
# Async mocking
@pytest.mark.asyncio
async def test_fetch_data():
with patch("myapp.client.fetch", new_callable=AsyncMock) as mock_fetch:
mock_fetch.return_value = {"status": "ok"}
result = await process_data()
assert result["status"] == "ok"
conftest.py 模式
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="session")
def engine():
return create_engine("sqlite:///:memory:")
@pytest.fixture(scope="function")
def db_session(engine):
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.rollback()
session.close()
Base.metadata.drop_all(engine)
Go 测试
表驱动测试
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
amount float64
code string
want float64
wantErr bool
}{
{
name: "valid 10% discount",
amount: 100.0,
code: "SAVE10",
want: 90.0,
},
{
name: "no discount",
amount: 100.0,
code: "",
want: 100.0,
},
{
name: "invalid code",
amount: 100.0,
code: "INVALID",
wantErr: true,
},
{
name: "zero amount",
amount: 0,
code: "SAVE10",
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CalculateDiscount(tt.amount, tt.code)
if (err != nil) != tt.wantErr {
t.Errorf("CalculateDiscount() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("CalculateDiscount() = %v, want %v", got, tt.want)
}
})
}
}
Testify 断言和模拟
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// Mock
type MockUserRepo struct {
mock.Mock
}
func (m *MockUserRepo) FindByID(id string) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func TestGetUser(t *testing.T) {
repo := new(MockUserRepo)
repo.On("FindByID", "123").Return(&User{ID: "123", Name: "John"}, nil)
service := NewUserService(repo)
user, err := service.GetUser("123")
require.NoError(t, err)
assert.Equal(t, "John", user.Name)
repo.AssertExpectations(t)
}
// HTTP handler testing
func TestGetUserHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/users/123", nil)
w := httptest.NewRecorder()
handler := NewHandler(mockService)
handler.GetUser(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var user User
err := json.NewDecoder(w.Body).Decode(&user)
require.NoError(t, err)
assert.Equal(t, "123", user.ID)
}
Rust 测试
单元测试和集成测试
// src/lib.rs - Unit tests (same file)
pub fn calculate_discount(amount: f64, percentage: f64) -> Result<f64, DiscountError> {
if percentage < 0.0 || percentage > 100.0 {
return Err(DiscountError::InvalidPercentage(percentage));
}
Ok(amount * (1.0 - percentage / 100.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_discount() {
let result = calculate_discount(100.0, 10.0).unwrap();
assert!((result - 90.0).abs() < f64::EPSILON);
}
#[test]
fn test_zero_discount() {
assert_eq!(calculate_discount(100.0, 0.0).unwrap(), 100.0);
}
#[test]
fn test_invalid_percentage() {
assert!(matches!(
calculate_discount(100.0, 150.0),
Err(DiscountError::InvalidPercentage(_))
));
}
#[test]
fn test_negative_percentage() {
assert!(calculate_discount(100.0, -10.0).is_err());
}
}
// tests/integration_test.rs - Integration tests (separate file)
use my_crate::calculate_discount;
#[test]
fn test_full_workflow() {
let original = 200.0;
let discounted = calculate_discount(original, 25.0).unwrap();
assert_eq!(discounted, 150.0);
}
基于属性的测试(proptest)
use proptest::prelude::*;
proptest! {
#[test]
fn discount_never_exceeds_original(amount in 0.0f64..10000.0, pct in 0.0f64..100.0) {
let result = calculate_discount(amount, pct).unwrap();
prop_assert!(result <= amount);
prop_assert!(result >= 0.0);
}
#[test]
fn roundtrip_serialization(name in "[a-zA-Z]{1,50}", age in 0u32..150) {
let user = User { name: name.clone(), age };
let json = serde_json::to_string(&user).unwrap();
let deserialized: User = serde_json::from_str(&json).unwrap();
prop_assert_eq!(user, deserialized);
}
}
视觉回归测试
Playwright 截图
import { test, expect } from "@playwright/test";
test("homepage visual regression", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("homepage.png", {
maxDiffPixelRatio: 0.01,
});
});
test("component states", async ({ page }) => {
await page.goto("/components");
// Default state
await expect(page.locator(".card")).toHaveScreenshot("card-default.png");
// Hover state
await page.locator(".card").hover();
await expect(page.locator(".card")).toHaveScreenshot("card-hover.png");
// Full page with specific viewport
await page.setViewportSize({ width: 375, height: 812 }); // iPhone
await expect(page).toHaveScreenshot("homepage-mobile.png");
});
// Update snapshots: npx playwright test --update-snapshots
Percy(云端视觉测试)
import percySnapshot from "@percy/playwright";
test("visual test with Percy", async ({ page }) => {
await page.goto("/dashboard");
await percySnapshot(page, "Dashboard");
// Percy compares across browsers and viewport sizes
});
Chromatic(Storybook 视觉测试)
# Run Chromatic on Storybook stories
npx chromatic --project-token=<token>
# CI integration
# Chromatic captures every story as a visual snapshot
# and detects pixel-level changes across PRs
| 工具 | 方法 | 最适合 |
|---|---|---|
| Playwright | 本地截图对比 | E2E 视觉测试、免费 |
| Percy | 云端跨浏览器对比 | 多浏览器、团队评审 |
| Chromatic | Storybook 故事快照 | 组件库视觉 QA |
另请参阅
- 代码审查标准 - 质量要求
- 项目
CLAUDE.md- 测试规则
评论
支持 Markdown