mcpField
T
test-coverage

test-coverage

github.com/cognitedata测试与质量61.9k 次安装1 天前更新
代码仓库

关于 test-coverage

test-coverage 是用于查找并修复 Flows 应用测试覆盖率缺口的技能,设有 80% 行覆盖率门槛。它指导开发者配置覆盖率、编写测试、补齐低覆盖部分并改进代码可测试性。

主要功能

  1. 0180% 行覆盖率门槛
  2. 02检查 Vitest 与 Jest 覆盖率配置
  3. 03检查生产代码的覆盖范围
  4. 04指导补充测试与修复缺口
  5. 05审查并重构可测试性

适用场景

  1. 01使 Flows 应用达到覆盖率门槛
  2. 02查找缺少测试的生产文件
  3. 03为未覆盖分支和错误路径补测试
  4. 04重构依赖以改善可测试性

安装

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

$npx skills add https://github.com/cognitedata/builder-skills --skill test-coverage

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

详细介绍

测试覆盖率修复

修复 $ARGUMENTS(如果未提供参数,则修复整个应用)的测试覆盖率。此技能通过查找并修复覆盖率缺口,确保达到 Flows 应用获批所要求的 80% 行覆盖率硬性门槛。请按顺序完成每个步骤。


步骤 1 — 验证测试框架和覆盖率工具

检查项目是否配置了可正常工作的测试框架和覆盖率工具:

# Check for vitest or jest in package.json
grep -E "(vitest|jest)" package.json

# Check for coverage configuration
cat vitest.config.ts 2>/dev/null || cat vitest.config.js 2>/dev/null || cat jest.config.ts 2>/dev/null || cat jest.config.js 2>/dev/null

验证以下事项:

  • 已安装并配置测试框架(Vitest 或 Jest)
  • 配置文件中包含 coverage 部分(例如,在 vitest.config.ts 中使用 coverage: { provider: 'v8', ... })
  • 已配置覆盖率报告器(至少包括 text 和 lcov 或 json-summary)

如果未配置覆盖率工具,请立即修复:

  1. 安装覆盖率提供程序:
pnpm add -D @vitest/coverage-v8
  1. 在 vitest.config.ts 中添加覆盖率配置。读取现有配置文件,然后在 test 内添加 coverage 部分:
// vitest.config.ts — minimum coverage configuration to add
test: {
  coverage: {
    provider: 'v8',
    reporter: ['text', 'text-summary', 'lcov'],
    include: ['src/**/*.{ts,tsx}'],
    exclude: [
      'src/**/*.test.{ts,tsx}',
      'src/**/*.spec.{ts,tsx}',
      'src/**/vite-env.d.ts',
      'src/main.tsx',
    ],
  },
}

写入更新后的配置文件。如果完全不存在 vitest.config.ts,请创建一个包含完整 defineConfig 包装器的配置文件。


步骤 2 — 验证覆盖率范围

80% 的阈值适用于 src/ 下的所有 .ts 和 .tsx 文件,仅排除:

  • 测试文件(*.test.ts、*.test.tsx、*.spec.ts、*.spec.tsx)
  • 类型声明文件(vite-env.d.ts)
  • 入口文件(main.tsx)

应用不得将页面、组件、钩子或其他生产代码排除在覆盖率统计之外。

# Check what files are excluded from coverage in the config
grep -A 20 "exclude" vitest.config.ts 2>/dev/null || grep -A 20 "exclude" vitest.config.js 2>/dev/null

# Check for coveragePathIgnorePatterns in jest config
grep -A 10 "coveragePathIgnorePatterns\|collectCoverageFrom" jest.config.ts 2>/dev/null

如果配置排除了生产文件,请立即修复:

移除所有会将生产代码排除在覆盖率统计之外的排除项。只能排除测试文件、类型声明文件和入口文件。将 exclude 数组改写为仅包含:

exclude: [
  'src/**/*.test.{ts,tsx}',
  'src/**/*.spec.{ts,tsx}',
  'src/**/vite-env.d.ts',
  'src/main.tsx',
],

具体而言,请移除以下排除项:

  • src/pages/、src/components/ 或 src/hooks/ — 不允许
  • 特定功能文件 — 不允许,除非它们是生成的代码
  • src/**/*.tsx(所有组件)— 不允许,这会隐藏应用中的大部分代码

写入修正后的配置文件。


步骤 3 — 运行测试并收集覆盖率

# Try common coverage commands based on project setup
npx vitest run --coverage 2>/dev/null || npx jest --coverage 2>/dev/null || npm test -- --coverage 2>/dev/null

记录覆盖率摘要:

  • 语句: X%
  • 分支: X%
  • 函数: X%
  • 行: X%

硬性门槛: 总体行覆盖率必须至少达到 80%。低于此阈值的应用会被列为必须修复。

如果测试无法运行,请立即修复:

常见修复方法:

  • 缺少导入: 阅读失败的测试文件,添加缺少的导入语句,并写入修复后的文件。
  • 模拟对象损坏: 阅读测试以了解模拟的内容。修复模拟对象,使其符合被模拟模块当前的 API。
  • 快照过时: 运行 npx vitest run --update 更新快照,然后检查差异以确保正确。
  • 缺少依赖: 对尚未安装的测试工具运行 pnpm add -D <missing-package>。
  • 配置错误: 阅读配置文件,修复语法或选项错误,并写入修正后的文件。

每次修复后重新运行测试,直到所有测试通过。然后记录覆盖率摘要。


步骤 4 — 查找并编写缺失的测试文件

对于 src/ 下每个非平凡的 .ts/.tsx 文件,检查是否存在对应的测试文件:

# List all production files and check for test counterparts
for file in $(find src -name "*.ts" -o -name "*.tsx" | grep -v ".test." | grep -v ".spec." | grep -v "node_modules" | grep -v "vite-env" | sort); do
  base="${file%.*}"
  ext="${file##*.}"
  dir=$(dirname "$file")
  filename=$(basename "$base")

  # Check for test file in same directory or __tests__ directory
  test_exists="false"
  for pattern in "${base}.test.${ext}" "${base}.spec.${ext}" "${base}.test.ts" "${base}.spec.ts" "${dir}/__tests__/${filename}.test.${ext}" "${dir}/__tests__/${filename}.spec.${ext}"; do
    if [ -f "$pattern" ]; then
      test_exists="true"
      break
    fi
  done

  if [ "$test_exists" = "false" ]; then
    echo "NO TEST: $file"
  fi
done

对每个没有测试的文件进行分类:

  • 服务、钩子、工具函数、上下文、ViewModel 钩子 — 立即编写测试文件(见下文)
  • 纯展示组件,不含逻辑 — 标记为 N/A(无需测试)
  • 汇总导出文件(只包含重新导出的 index.ts)— 标记为 N/A
  • 仅包含类型的文件(.d.ts,或仅导出类型/接口的文件)— 标记为 N/A

为每个缺少测试的文件创建全面的测试文件。 如果生产代码支持,请使用上下文注入来模拟依赖项。如果生产代码使用硬编码导入,请将其注明为可测试性问题,但仍使用 vi.mock 编写测试,并附上理由注释。对每个文件遵循以下流程:

  1. 阅读源文件,了解其导出内容、依赖项和逻辑。
  2. 创建 .test.ts 或 .test.tsx 文件,放在与源文件相同的目录中。
  3. **编写测试,覆盖:**正常路径、错误路径、空状态和边缘情况。

针对不同文件类型使用合适的测试模式:

针对钩子:

  • 使用 @testing-library/react 中的 renderHook 进行测试
  • 使用必要的 Provider 包装(QueryClientProvider、自定义上下文 Provider 等)
  • 测试初始状态、加载状态、成功状态和错误状态
  • 示例结构:
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// import the hook

const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

describe('useMyHook', () => {
  it('returns data on success', async () => {
    const { result } = renderHook(() => useMyHook(), { wrapper: createWrapper() });
    await waitFor(() => expect(result.current.data).toBeDefined());
  });

  it('handles errors', async () => {
    // set up error condition
    const { result } = renderHook(() => useMyHook(), { wrapper: createWrapper() });
    await waitFor(() => expect(result.current.error).toBeDefined());
  });
});

针对服务/工具函数:

  • 直接调用函数进行测试
  • 必要时模拟 CDF SDK 响应
  • 测试返回值、副作用和抛出的错误
  • 示例结构:
import { describe, it, expect, vi } from 'vitest';
// import the service/util functions

describe('myService', () => {
  it('returns expected result for valid input', () => {
    const result = myFunction(validInput);
    expect(result).toEqual(expectedOutput);
  });

  it('throws on invalid input', () => {
    expect(() => myFunction(invalidInput)).toThrow();
  });
});

针对包含逻辑的组件:

  • 使用 @testing-library/react 中的 render 进行测试
  • 验证加载、错误和数据状态
  • 测试会触发状态变化的用户交互
  • 示例结构:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// import the component and providers

describe('MyComponent', () => {
  it('shows loading state initially', () => {
    render(<MyComponent />, { wrapper: createWrapper() });
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
  });

  it('renders data after loading', async () => {
    render(<MyComponent />, { wrapper: createWrapper() });
    await waitFor(() => {
      expect(screen.getByText('expected content')).toBeInTheDocument();
    });
  });
});

依赖项模拟指南:

  • 尽可能使用上下文注入(而不是 vi.mock)——通过钩子的上下文提供测试依赖项。
  • 如果生产代码使用硬编码导入,导致无法进行上下文注入,请使用 vi.mock,并附上理由注释,说明原因(例如:// vi.mock required: useDataSource uses direct import, not context injection)。
  • 确保模拟对象具备类型安全性——不要使用 as unknown as T 类型断言。定义符合接口要求的模拟对象。

编写完每个测试文件后,运行 npx vitest run <test-file> 验证测试通过。


步骤 5 — 修复覆盖率较低的文件

如果覆盖率工具提供了各文件的指标,请列出行覆盖率低于 80% 的文件:

# Parse lcov or text output for per-file coverage
cat coverage/coverage-summary.json 2>/dev/null | node -e "
  const data = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
  Object.entries(data).forEach(([file, metrics]) => {
    if (file === 'total') return;
    const pct = metrics.lines?.pct ?? 0;
    if (pct < 80) console.log(pct.toFixed(1) + '% — ' + file);
  });
" 2>/dev/null

对于每个覆盖率低于 80% 的文件,先从覆盖率报告中读取未覆盖的代码行,然后添加测试用例,覆盖这些特定代码路径:

  1. 读取覆盖率报告中的未覆盖代码行。 检查 coverage/ 目录中的详细文件报告(lcov 或 html),确认哪些行未被覆盖。
  2. 阅读源文件,了解这些未覆盖代码行的作用。
  3. 添加测试用例,覆盖这些特定代码路径:
  • **未覆盖的错误路径:**添加可触发错误条件的测试(网络故障、无效输入、空值、权限错误)。通过提供错误输入或模拟依赖项抛出错误来触发错误。
  • **未覆盖的分支:**为每个条件分支添加测试。如果 if/else 只测试了 true 分支,就编写测试来触发 false 分支。
  • **未覆盖的函数:**为每个缺少覆盖率的导出函数添加测试。验证返回值和副作用。
  • **未覆盖的 catch 块:**模拟上游调用拒绝或抛出错误,并验证 catch 块的行为。
  1. 添加测试后再次运行覆盖率,验证该文件现在是否达到 80%:
npx vitest run --coverage <test-file>

重复上述步骤,直到每个文件的行覆盖率至少达到 80%,或已覆盖所有可行的路径。


第 6 步 — 修复可测试性模式

评估并修复代码库中的可测试性问题:

# Check for dependency injection via context
grep -rn --include="*.ts" --include="*.tsx" "useContext\|createContext" src/hooks/ src/contexts/

# Check for vi.mock usage (red flag for testability)
grep -rn --include="*.ts" --include="*.tsx" "vi\.mock" src/

# Check for unsafe casts in tests
grep -rn --include="*.ts" --include="*.tsx" "as unknown as" src/ | grep -E "\.test\.|\.spec\."

# Check for interface-based services
grep -rn --include="*.ts" --include="*.tsx" -E "implements\s+\w+" src/

对于发现的每个可测试性问题,重构生产代码以支持更好的测试模式。然后更新相应的测试,使用改进后的模式。

问题修复方法
Hook 直接导入依赖,而不是使用上下文添加带有默认依赖项的上下文类型。使用 createContext 创建上下文,并提供使用真实实现的默认值。在 Hook 中,使用 useContext 获取依赖项。这样,测试就可以通过上下文提供程序传入模拟依赖项,而无需使用 vi.mock。
服务没有接口提取描述服务公共 API 的 TypeScript 接口。让类或对象实现该接口。测试应针对接口进行模拟,而不是针对具体实现。
页面组件将数据获取与渲染混在一起将数据逻辑提取到 use*ViewModel Hook 中。页面组件调用 ViewModel Hook,并根据其返回值进行渲染。使用 renderHook 单独测试 ViewModel Hook,并通过模拟 ViewModel 测试页面组件。
测试对可通过上下文注入依赖的模块使用 vi.mock将生产代码重构为使用上下文注入(如上所述)后,更新测试,通过上下文提供程序传入模拟依赖项。移除 vi.mock 调用。添加注释说明此模式。
测试使用 as unknown as T 类型断言来创建模拟对象定义符合所需接口的正确模拟类型或对象。用类型正确的模拟对象替换类型断言。如果接口较大,可创建一个辅助函数,返回仅包含所用方法且类型正确的部分模拟对象。

对于每个重构的文件:

  1. 阅读源文件及其测试文件。
  2. 重构生产代码(添加上下文、提取接口、提取 ViewModel Hook)。
  3. 更新测试,使其使用改进后的模式。
  4. 运行 npx vitest run <test-file>,验证测试仍然通过。

第 7 步 — 报告剩余差距

重新运行完整测试套件并启用覆盖率,以获取最终数据:

npx vitest run --coverage 2>/dev/null || npx jest --coverage 2>/dev/null

总结已完成的工作和剩余事项:

### Test Coverage Summary (After Fixes)

| Metric | Before | After | Gate |
|--------|--------|-------|------|
| Lines | X% | Y% | ≥80% required |
| Branches | X% | Y% | — |
| Functions | X% | Y% | — |
| Statements | X% | Y% | — |

### Coverage verdict: PASS / FAIL

### What was fixed
- [ ] Coverage tooling configured/corrected
- [ ] Exclusions cleaned up (removed N production file exclusions)
- [ ] N failing tests fixed
- [ ] N new test files written (list them)
- [ ] N existing test files expanded for coverage
- [ ] N files refactored for testability

### Remaining gaps (needs human review)
Only list issues that could not be auto-fixed:
- Complex business logic where correct test assertions require domain knowledge
- Integration tests that need real API credentials or environment setup
- Files where coverage cannot reach 80% without major architectural changes (explain why)

完成

总结:

  • 修复前后总体覆盖率,以及与 80% 门槛的比较(PASS 或 FAIL)
  • 编写的测试文件数和新增的测试数
  • 为提高可测试性而重构的文件数
  • 任何仍需人工审核的必须修复事项

评论

支持 Markdown