mcpField
T
test-specialist

test-specialist

github.com/travisjneuman测试与质量106134 次安装今天更新
代码仓库

关于 test-specialist

面向 JavaScript/TypeScript、Python、Go 和 Rust 项目的测试编写技能,适用于用户明确要求测试时。它提供单元、集成、端到端、安全和性能测试模式,并包含缺陷分析流程。

主要功能

  1. 01涵盖 JS/TS、Python、Go、Rust
  2. 02单元、集成与端到端测试模式
  3. 03组件测试与行为验证
  4. 04安全和性能测试示例
  5. 05结构化缺陷分析流程

适用场景

  1. 01按请求为功能编写测试
  2. 02补充关键路径测试覆盖
  3. 03编写安全或性能测试
  4. 04通过复现和隔离分析缺陷

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/travisjneuman/.claude --skill test-specialist

安装后在 Claude Code 中输入/test-specialist确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

index.js
package.json
references/
bug_analysis.md
testing_patterns.md
scripts/
analyze_coverage.py
find_untested_code.py

详细介绍

测试专家

适用于 JS/TS、Python、Go 和 Rust 的测试编写模式,以及在用户要求编写测试时适用的 Bug 分析习惯。

使用时机

仅当用户明确要求编写测试时,才使用此技能。若用户没有提出此要求,请通过审查差异及其涉及的代码来验证。

适用于(用户要求编写测试时):

  • 编写单元测试、集成测试或端到端测试
  • 提高测试覆盖率
  • 安全和性能测试

不适用于:

  • 未要求编写测试的调试 → 使用 debug-systematic
  • 代码审查 → 使用 generic-code-reviewer
  • 技术债务 → 使用 tech-debt-analyzer
  • 功能开发 → 使用 generic-feature-developer

按项目类型划分的测试栈

项目类型单元测试组件端到端
React/Next.jsVitest/JestTesting LibraryPlaywright
Node.jsVitest/JestSupertestPlaywright
静态项目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 分析流程

  1. 复现 - 记录准确步骤、预期结果与实际结果
  2. 隔离 - 二分查找,创建最小复现
  3. 查明根因 - 跟踪执行过程、检查假设、使用 git blame
  4. 修复 - 修复根因(仅在用户要求编写测试时,才先编写失败测试)
  5. 验证 - 根据复现步骤和边界情况审查差异及其涉及的代码路径

调试检查清单

调试问题时:

  • 能否稳定复现
  • 是否已创建最小复现
  • 是否已检查控制台和网络日志
  • 是否已检查失败位置的状态
  • 是否已通过 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%+

测试质量原则

  1. 每个测试验证一种行为
  2. 名称清晰 - 测试名称应说明场景
  3. 测试相互独立 - 不共享状态
  4. 覆盖边界情况 - 空值、空内容、边界和错误
  5. 模拟外部依赖 - 测试应快速运行
  6. 测试行为 - 而非实现细节

工作流决策树

情况操作
添加功能(用户要求编写测试)先编写测试(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云端跨浏览器对比多浏览器、团队评审
ChromaticStorybook 故事快照组件库视觉 QA

另请参阅

评论

支持 Markdown