测试专家Skill test-specialist

测试专家技能专注于JavaScript/TypeScript应用程序的全面测试、调试和代码分析,包括单元测试、集成测试、端到端测试、安全测试、性能测试,提供系统化方法修复bug、提高测试覆盖率,并涵盖多种编程语言如Python、Go、Rust的测试模式。关键词:JavaScript测试、TypeScript测试、单元测试、集成测试、E2E测试、调试、代码分析、测试覆盖率、安全测试、性能测试、视觉回归测试、pytest、Testify、Playwright、Percy、Chromatic、SEO优化、软件质量保证、测试自动化、前端测试、后端测试、移动测试、DevOps测试。

测试 0 次安装 0 次浏览 更新于 3/20/2026

name: 测试专家 description: 这个技能应该用于编写测试用例、修复bug、分析代码潜在问题或提高JavaScript/TypeScript应用程序的测试覆盖率。适用于单元测试、集成测试、端到端测试、调试运行时错误、逻辑bug、性能问题、安全漏洞和系统代码分析。

测试专家

JS/TS应用程序的系统化测试方法和调试技术。

何时使用

适用于:

  • 编写单元测试、集成测试或端到端测试
  • 修复bug和调试
  • 提高测试覆盖率
  • 分析代码潜在问题
  • 安全测试和性能测试

不适用于:

  • 代码审查 → 使用 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

测试模式

单元测试 (AAA模式)

describe("calculateTotal", () => {
  test("正确求和金额", () => {
    // 安排
    const items = [{ amount: 100 }, { amount: 50 }];
    // 执行
    const total = calculateTotal(items);
    // 断言
    expect(total).toBe(150);
  });

  test("处理空列表", () => {
    expect(calculateTotal([])).toBe(0);
  });
});

组件测试 (用户行为)

// ✅ 测试用户行为,而非实现细节
it('用户点击添加时创建项目', async () => {
  const user = userEvent.setup();
  render(<ItemList />);

  await user.click(screen.getByRole('button', { name: /add/i }));
  await user.type(screen.getByLabelText(/title/i), '新项目');
  await user.click(screen.getByRole('button', { name: /save/i }));

  expect(screen.getByText('新项目')).toBeInTheDocument();
});

端到端测试 (Playwright)

import { test, expect } from "@playwright/test";

test("用户可以完成结账", async ({ page }) => {
  await page.goto("/products");

  // 添加到购物车
  await page.click('button:has-text("添加到购物车")');
  await page.click('a:has-text("购物车")');

  // 结账
  await page.click('button:has-text("结账")');
  await page.fill('[name="email"]', "test@example.com");
  await page.click('button:has-text("下订单")');

  // 验证
  await expect(page.locator("h1")).toContainText("订单确认");
});

集成测试

test("POST /items 创建项目", async () => {
  const response = await request(app)
    .post("/api/items")
    .send({ name: "测试" })
    .expect(201);

  expect(response.body).toMatchObject({ id: expect.any(Number) });
});

Bug分析流程

  1. 重现 - 记录确切步骤、预期与实际结果
  2. 隔离 - 二分查找、最小化重现
  3. 根因 - 追踪执行、检查假设、git blame
  4. 修复 - 先写失败测试、实现修复
  5. 验证 - 运行完整套件、测试边界情况

调试清单

当调试问题时:

  • [ ] 能否一致重现
  • [ ] 创建了最小化重现
  • [ ] 检查了控制台/网络日志
  • [ ] 检查了失败点的状态
  • [ ] 检查了git blame以查看最近更改
  • [ ] 在修复前编写了失败测试

常见Bug模式

竞态条件

test("处理并发更新", async () => {
  const promises = Array.from({ length: 100 }, () => increment());
  await Promise.all(promises);
  expect(getCount()).toBe(100);
});

空安全性

test.each([null, undefined, "", 0])("处理无效输入: %p", (input) => {
  expect(() => process(input)).toThrow("无效");
});

边界值

test("处理边界情况", () => {
  expect(paginate([], 1, 10)).toEqual([]); // 空列表
  expect(paginate([item], 1, 10)).toEqual([item]); // 单个项目
  expect(paginate(items25, 3, 10)).toHaveLength(5); // 部分最后一页
});

安全测试

test("防止SQL注入", async () => {
  const malicious = "'; DROP TABLE users; --";
  await expect(search(malicious)).resolves.not.toThrow();
});

test("净化XSS", () => {
  const xss = '<script>alert("xss")</script>';
  expect(sanitize(xss)).not.toContain("<script>");
});

test("需要认证", async () => {
  await request(app).post("/api/items").expect(401);
});

性能测试

test("高效处理大型数据集", () => {
  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. 覆盖边界情况 - null、空、边界、错误
  5. 模拟外部依赖 - 测试应快速
  6. 测试行为 - 而非实现细节

工作流程决策树

情况 行动
添加功能 先写测试 (TDD)
修复bug 写失败测试,然后修复
提高覆盖率 找到差距,优先关键路径
代码审查 检查边界情况、错误处理

Python测试 (pytest)

夹具和参数化

import pytest
from myapp.services import UserService

@pytest.fixture
def user_service(db_session):
    """提供带测试数据库的UserService。"""
    return UserService(session=db_session)

@pytest.fixture
def sample_user(user_service):
    """创建并返回示例用户。"""
    return user_service.create(name="测试用户", email="test@example.com")

class TestUserService:
    def test_create_user(self, user_service):
        user = user_service.create(name="John", email="john@example.com")
        assert user.name == "John"
        assert user.id is not None

    def test_get_user_not_found(self, user_service):
        with pytest.raises(UserNotFoundError, match="用户 999 未找到"):
            user_service.get(999)

    @pytest.mark.parametrize("email,is_valid", [
        ("user@example.com", True),
        ("user@sub.domain.com", 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="你好")
        mock_email.send.assert_called_once_with(
            to="test@example.com",
            body="你好",
        )

# 异步模拟
@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:   "有效的10%折扣",
            amount: 100.0,
            code:   "SAVE10",
            want:   90.0,
        },
        {
            name:   "无折扣",
            amount: 100.0,
            code:   "",
            want:   100.0,
        },
        {
            name:    "无效代码",
            amount:  100.0,
            code:    "INVALID",
            wantErr: true,
        },
        {
            name:   "零金额",
            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() 错误 = %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"
)

// 模拟
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处理器测试
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 - 单元测试(同一文件)
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 - 集成测试(单独文件)
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("首页视觉回归", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("homepage.png", {
    maxDiffPixelRatio: 0.01,
  });
});

test("组件状态", async ({ page }) => {
  await page.goto("/components");

  // 默认状态
  await expect(page.locator(".card")).toHaveScreenshot("card-default.png");

  // 悬停状态
  await page.locator(".card").hover();
  await expect(page.locator(".card")).toHaveScreenshot("card-hover.png");

  // 全页面,指定视口
  await page.setViewportSize({ width: 375, height: 812 }); // iPhone
  await expect(page).toHaveScreenshot("homepage-mobile.png");
});

// 更新快照: npx playwright test --update-snapshots

Percy (云视觉测试)

import percySnapshot from "@percy/playwright";

test("使用Percy的视觉测试", async ({ page }) => {
  await page.goto("/dashboard");
  await percySnapshot(page, "仪表板");
  // Percy跨浏览器和视口尺寸比较
});

Chromatic (Storybook视觉测试)

# 在Storybook故事上运行Chromatic
npx chromatic --project-token=<token>

# CI集成
# Chromatic捕获每个故事作为视觉快照,并在PR中检测像素级更改
工具 方法 最适合
Playwright 本地截图比较 端到端视觉测试,免费
Percy 云端跨浏览器比较 多浏览器、团队审查
Chromatic Storybook故事快照 组件库视觉QA

另请参阅