mcpField
S
snapshot-test-refactorer

snapshot-test-refactorer

github.com/patricio0312rev测试与质量63215 次安装41 周前更新
代码仓库

关于 snapshot-test-refactorer

snapshot-test-refactorer 是一项改进快照测试的技能,面向维护测试套件的开发者。它建议使用聚焦断言、精简或内联快照,并以视觉回归测试检查界面外观。

主要功能

  1. 01用具体断言替代宽泛快照
  2. 02为数据使用内联快照
  3. 03将快照缩减至关键内容
  4. 04用序列化器移除不稳定字段
  5. 05通过视觉回归测试检查界面

适用场景

  1. 01重构脆弱的组件测试
  2. 02检查 API 响应中的关键字段
  3. 03用 Playwright 检查界面外观

安装

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

$npx skills add https://github.com/patricio0312rev/skills --skill snapshot-test-refactorer

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

详细介绍

快照测试重构工具

用有意义且易于维护的断言替换脆弱的快照。

快照测试的问题

// ❌ Bad: Full component snapshot
test("renders UserProfile", () => {
  const { container } = render(<UserProfile user={mockUser} />);
  expect(container).toMatchSnapshot();
});

// Problems:
// 1. Fails on any change (even whitespace)
// 2. No clear intent
// 3. Hard to review diffs
// 4. Doesn't test behavior
// 5. Implementation coupled

重构策略

// ✅ Good: Specific assertions
test("renders UserProfile with user data", () => {
  render(<UserProfile user={mockUser} />);

  // Test what matters
  expect(screen.getByText(mockUser.name)).toBeInTheDocument();
  expect(screen.getByText(mockUser.email)).toBeInTheDocument();
  expect(screen.getByRole("img")).toHaveAttribute("src", mockUser.avatar);
});

test("shows edit button for own profile", () => {
  render(<UserProfile user={mockUser} isOwnProfile={true} />);

  expect(
    screen.getByRole("button", { name: "Edit Profile" })
  ).toBeInTheDocument();
});

test("hides edit button for other profiles", () => {
  render(<UserProfile user={mockUser} isOwnProfile={false} />);

  expect(
    screen.queryByRole("button", { name: "Edit Profile" })
  ).not.toBeInTheDocument();
});

用于数据的内联快照

// ❌ Bad: External snapshot file
test("formats user data", () => {
  const result = formatUser(mockUser);
  expect(result).toMatchSnapshot();
});

// ✅ Good: Inline snapshot (visible in code)
test("formats user data", () => {
  const result = formatUser(mockUser);
  expect(result).toMatchInlineSnapshot(`
    {
      "displayName": "John Doe",
      "initials": "JD",
      "memberSince": "2020-01-01",
    }
  `);
});

部分快照

// ❌ Bad: Snapshot entire API response
test("fetches user", async () => {
  const response = await api.getUser("123");
  expect(response).toMatchSnapshot();
});

// ✅ Good: Test important parts
test("fetches user with required fields", async () => {
  const response = await api.getUser("123");

  expect(response).toMatchObject({
    id: "123",
    email: expect.stringContaining("@"),
    role: expect.any(String),
  });

  // Snapshot only stable, important data
  expect({
    name: response.name,
    role: response.role,
  }).toMatchInlineSnapshot(`
    {
      "name": "John Doe",
      "role": "USER",
    }
  `);
});

不稳定数据的序列化器

// Remove unstable fields before snapshot
expect.addSnapshotSerializer({
  test: (val) => val && typeof val === "object" && "createdAt" in val,
  serialize: (val) => {
    const { createdAt, updatedAt, ...rest } = val;
    return JSON.stringify(rest, null, 2);
  },
});

// Now timestamps won't break tests
test("creates user", async () => {
  const user = await createUser({ name: "Test" });

  expect(user).toMatchInlineSnapshot(`
    {
      "id": "123",
      "name": "Test",
      "role": "USER"
    }
  `);
  // createdAt automatically removed
});

快照精简策略

// Before: 500 line snapshot
expect(component).toMatchSnapshot();

// After: Focus on critical parts
const criticalElements = {
  header: screen.getByRole("banner").textContent,
  mainAction: screen.getByRole("button", { name: /submit/i }).textContent,
  errorMessage: screen.queryByRole("alert")?.textContent,
};

expect(criticalElements).toMatchInlineSnapshot(`
  {
    "errorMessage": null,
    "header": "Welcome",
    "mainAction": "Submit",
  }
`);

视觉回归替代方案

// Instead of DOM snapshot, use visual regression
test("Profile component appearance", async ({ page }) => {
  await page.goto("/profile");

  // Visual snapshot (Playwright)
  await expect(page).toHaveScreenshot("profile.png", {
    maxDiffPixels: 100,
  });
});

何时可以使用快照

// ✅ OK: Error messages (rarely change)
test("validates email format", () => {
  const errors = validateEmail("invalid");
  expect(errors).toMatchInlineSnapshot(`
    [
      "Email must contain @",
      "Email must contain domain",
    ]
  `);
});

// ✅ OK: API response structure (stable contract)
test("user API response structure", async () => {
  const response = await api.getUser("123");

  expect(Object.keys(response).sort()).toMatchInlineSnapshot(`
    [
      "createdAt",
      "email",
      "id",
      "name",
      "role",
      "updatedAt",
    ]
  `);
});

// ✅ OK: Serialized data format
test("exports user to JSON", () => {
  const json = exportUserToJSON(user);
  expect(json).toMatchInlineSnapshot(`
    {
      "email": "[email protected]",
      "name": "John Doe",
      "version": "1.0",
    }
  `);
});

重构流程

# Snapshot Refactoring Checklist

For each snapshot test, ask:

1. **What is being tested?**

   - If unclear → Replace with specific assertions

2. **Does it test behavior or implementation?**

   - Implementation → Refactor to behavior test

3. **How often does this change?**

   - Frequently → Use targeted assertions
   - Rarely → Snapshot OK

4. **Can I describe what should pass/fail?**

   - No → Snapshot is too broad

5. **Would a visual test be better?**
   - UI appearance → Use screenshot testing

## Refactoring Steps

1. Run snapshot test, let it fail
2. Look at the diff
3. Extract what actually matters
4. Write assertion for that specific thing
5. Delete snapshot
6. Repeat for next snapshot

重构示例

// ❌ Before: Brittle 200-line snapshot
test("renders dashboard", () => {
  const { container } = render(<Dashboard user={user} />);
  expect(container).toMatchSnapshot();
});

// ✅ After: Multiple focused tests
describe("Dashboard", () => {
  test("displays welcome message with user name", () => {
    render(<Dashboard user={user} />);
    expect(screen.getByText(`Welcome back, ${user.name}!`)).toBeInTheDocument();
  });

  test("shows user stats", () => {
    render(<Dashboard user={user} stats={mockStats} />);

    expect(screen.getByText(`${mockStats.orders} orders`)).toBeInTheDocument();
    expect(screen.getByText(`$${mockStats.revenue}`)).toBeInTheDocument();
  });

  test("displays quick actions", () => {
    render(<Dashboard user={user} />);

    expect(
      screen.getByRole("button", { name: "New Order" })
    ).toBeInTheDocument();
    expect(
      screen.getByRole("button", { name: "View Reports" })
    ).toBeInTheDocument();
  });

  test("shows empty state when no recent activity", () => {
    render(<Dashboard user={user} recentActivity={[]} />);

    expect(screen.getByText("No recent activity")).toBeInTheDocument();
  });
});

自动转换脚本

// scripts/convert-snapshots.ts
import * as fs from "fs";
import * as path from "path";

function convertSnapshotToAssertions(testFile: string): string {
  let content = fs.readFileSync(testFile, "utf-8");

  // Replace toMatchSnapshot() with specific assertions
  content = content.replace(
    /expect\((.+?)\)\.toMatchSnapshot\(\)/g,
    (match, element) => {
      return `// TODO: Replace with specific assertions
// expect(${element}).to... `;
    }
  );

  return content;
}

维护策略

# Snapshot Maintenance Guidelines

## When to Update Snapshots

✅ **Update when:**

- Intentional design change
- New feature added
- Bug fix that changes output
- Refactoring that changes structure

❌ **Don't update when:**

- "Jest said to update"
- Test is failing
- Don't understand the change
- Too lazy to investigate

## Review Process

1. Run `jest -u` to update
2. Review EVERY changed snapshot
3. Verify change is intentional
4. If unsure, ask for review
5. Consider if assertion would be better

## Reduce Snapshot Size

- Use `.toMatchObject()` for partial matches
- Extract only relevant data
- Use serializers to remove noise
- Consider inline snapshots

最佳实践

  1. 内联快照:更直观,也更便于审查
  2. 小型快照:只对重要内容创建快照
  3. 稳定数据:移除时间戳、ID
  4. 明确意图:测试名称应说明捕获的内容
  5. 视觉回归:用于检查 UI 外观
  6. 定期审查:每季度审查一次快照
  7. 具体断言:优先使用具体断言,而非快照

输出检查清单

  • 已识别脆弱的快照
  • 已重构为具体断言
  • 已在适当情况下使用内联快照
  • 已移除不稳定数据(序列化器)
  • 已对数据结构使用部分快照
  • 已对 UI 使用视觉回归
  • 已记录维护指南
  • 已建立审查流程

评论

支持 Markdown