mcpField
F
find-untested-sources

find-untested-sources

github.com/dotnet测试与质量5.8k3.2k 次安装1 天前更新
代码仓库

关于 find-untested-sources

用于在编写测试前查找未与测试文件配对的源文件的静态分析技能。提供 Roslyn 和 tree-sitter 两种解析引擎,无需构建或运行测试即可生成配对清单。

主要功能

  1. 01提供 Roslyn 与 tree-sitter 引擎
  2. 02支持 C# 及多种语言
  3. 03静态解析源文件与测试文件
  4. 04输出配对和未配对清单
  5. 05提供建议的测试文件路径

适用场景

  1. 01为新增测试确定候选源文件
  2. 02检查生成的测试是否关联到源文件
  3. 03在多语言仓库中整理测试缺口
  4. 04识别没有对应源文件的测试

安装

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

$npx skills add https://github.com/dotnet/skills --skill find-untested-sources

安装后在 Claude Code 中输入/find-untested-sources确认已加载。

附带文件scripts/find_untested_sources.pyscripts/Find-UntestedSources.cs

详细介绍

查找未经测试的源文件

用途

覆盖率工具回答的是“哪些代码行执行过?”,它们要求构建成功且测试运行通过;在真实代码仓库中,这可能需要几分钟到几十分钟。本技能要回答的问题则不同,而且成本低得多:

哪些源文件没有任何测试文件引用其中声明的类型/符号?

这是 Agent 在编写新测试之前会提出的问题;只需解析源文件,就能在几秒内通过静态分析得出答案,无需构建、解析依赖项或编译。输出是一份确定性的测试配对映射,让 Agent 无需先通读整个代码库,就能选出下一个要测试的文件。

两种引擎——任选其一

此技能提供两个可互换的分析器,且采用兼容的 JSON 契约:

引擎脚本适用情况
Roslyn(C#)scripts/Find-UntestedSources.cs代码仓库仅包含 .NET。使用 Roslyn 语法 API 解析每个 .cs 文件,并进行严格的命名空间消歧,因此在 Settings 或 Context 这类短名称重复的情况下,准确性明显更高。
tree-sitter(多语言)scripts/find_untested_sources.py代码仓库不只包含 C#,或者你希望使用一个工具处理 C#、Python、TypeScript/JavaScript、Go、Java、Rust、Ruby、Kotlin、Swift、PowerShell 和 C++。

对于仅包含 .NET 的代码仓库,优先使用 Roslyn 引擎——它基于命名空间的配对比多语言引擎的标识符重叠配对更准确。

必需的工作流

  1. 使用调用者指定的最小范围代码仓库或包根目录。如果请求指定了子目录,不要扫描父级工作区。
  2. 执行一次适用的分析器。不要用手动 glob 匹配、文件名匹配或目视检查来代替执行分析器。 进行多语言分析时,如果答案必须区分已配对和未配对的源文件,请传入 --include-tested。 “仅进行静态配对”禁止编译目标代码仓库或运行其中的测试;但不禁止启动此技能的纯解析分析器。如果调用者还说“不要构建”,请简要说明这一区别。 将分析器依赖视为环境前置条件:不要安装软件包、尝试使用错误的引擎、构建代码仓库,也不要在分析器调用失败时退而进行手动扫描。应报告前置条件失败。
  3. 根据分析器的 JSON 得出结果。保留其配对/未配对分类和建议的相对路径;不要猜测其他路径。
  4. 如果调用者指定了子目录,请在分析器生成的相对路径前加上该子目录,使报告中的路径相对于工作区。
  5. 报告所请求的结果,并说明静态配对覆盖率的注意事项。不要附加构建、安装软件包、运行测试或覆盖率命令。如果存在已配对的源文件,请列出覆盖它们的测试文件,以便核查未配对分类。

适用情况

  • 用户询问“根据源文件配对情况,我应该在哪里添加测试?”、“哪些文件没有测试?”、“查找未配对的源文件”或“给我一份静态测试缺口清单”。
  • 在调用测试生成 Agent 之前,生成源文件配对任务清单。
  • 生成测试之后,验证每个新测试文件是否与源文件配对。
  • 枚举“弱配对”的源文件(只有一个引用它的测试),以便后续深入检查。

不适用情况

  • 行/分支覆盖率——使用 coverage-analysis。
  • 基于真实覆盖率数据确定优先级——使用 coverage-analysis。
  • CRAP 分数/风险热点——使用 coverage-analysis。
  • 现有测试是否足够强?——使用 test-gap-analysis(变异推理)或 assertion-quality。

Roslyn 引擎(C#)

前提条件
  • 支持基于文件的应用(dotnet run script.cs)的 .NET SDK。版本由仓库中的 global.json 固定(SDK 11 预览版或更高版本)。
  • 除首次运行时还原 Microsoft.CodeAnalysis.CSharp 的 NuGet 包外,无需访问互联网。
用法
# From the skill folder
dotnet run scripts/Find-UntestedSources.cs -- <repo-root> [--top N]

# Save the report
dotnet run scripts/Find-UntestedSources.cs -- <repo-root> > pairing.json

# Iterate the untested list, highest-API-surface first
$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested | Select-Object -First 10 source, decl_count, suggested_test_path

诊断信息输出到 stderr;JSON 输出到 stdout。

输出架构
{
  "repo": "<absolute path>",
  "elapsed_ms": 8883,
  "counts": {
    "source_files": 3036,
    "test_files": 867,
    "untested_files": 1852,
    "paired_files": 1184
  },
  "untested": [
    {
      "source": "src/Foo/Bar.cs",
      "decl_count": 8,            // # of type declarations in the file
      "suggested_test_path":      // mirror of source under a discovered test project
        "tests/Foo.Tests/Bar/BarTests.cs"
    }
  ],
  "source_to_tests": {
    "src/Foo/Baz.cs": [
      "tests/Foo.Tests/BazTests.cs",
      "tests/Foo.IntegrationTests/Scenarios/BazScenarios.cs"
    ]
  }
}
工作原理
  1. 文件发现 — 递归遍历目录,排除 bin/、obj/、node_modules/、 .git/、.vs/、packages/ 以及所有名称以点开头的子目录。跳过生成的文件 (.g.cs、.Designer.cs、.AssemblyInfo.cs)。
  2. 测试与源代码分类 — 向上遍历至最近的 .csproj,如果项目名称以 .Tests、.Test、 .UnitTests、.IntegrationTests、.E2E、.EndToEnd、.Spec、.Specs 结尾, 或内容引用了 Microsoft.NET.Test.Sdk、MSTest.Sdk、 Microsoft.Testing.Platform、xunit、NUnit、TUnit,或包含 <IsTestProject>true</IsTestProject>,则将其标记为测试项目。
  3. 源代码索引(并行) — 使用 CSharpSyntaxTree.ParseText 解析每个源文件 (仅语法,不进行编译);将每个 BaseTypeDeclarationSyntax / DelegateDeclarationSyntax 记录为 (ShortName, EnclosingNamespace, FilePath)。
  4. 测试扫描(并行) — 解析每个测试文件,收集 using 指令和 封闭命名空间,遍历所有 IdentifierToken,并在短名称索引中查找;然后严格消歧: 只有当声明的命名空间与测试文件的某个 using 指令、封闭命名空间或其前缀匹配时, 才会将标识符归属到该声明。这可避免 Settings 或 Context 等常见名称与每个项目都匹配所产生的噪声。
  5. 配对与建议 — 反转映射,得到 source → [tests]。根据 <ProjectReference> 条目构建 生产项目到测试项目的映射;对每个未经测试的源文件,将其项目内相对路径映射到引用它的测试项目下,以此建议路径。
  6. 输出 JSON — 按声明数量降序排列,然后按字母顺序排列。

多语言引擎(tree-sitter)

前提条件
  • Python 3.10+。
  • pip install tree-sitter-language-pack(一个自包含的 wheel,内置 300 多种语言的解析器和高层 process() API)。无需本地构建,也无需逐个安装语言语法。
用法
# From the skill folder
python scripts/find_untested_sources.py <repo-root>

# Restrict to a language (repeatable)
python scripts/find_untested_sources.py <repo-root> --lang python --lang typescript

# Truncate the report (top 20 by declared API surface)
python scripts/find_untested_sources.py <repo-root> --limit-untested 20 > pairing.json

# Iterate, highest-API-surface first
$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested_sources | Select-Object -First 10 path, declaration_count, suggested_test_path

传入 --include-tested 可额外输出 tested_sources(默认省略,以减小供大模型使用的载荷)。诊断信息输出到 stderr;JSON 输出到 stdout。

输出架构
{
  "repo_root": "<absolute path>",
  "summary": {
    "source_files": 3138,
    "test_files": 761,
    "tested_source_files": 1419,
    "untested_source_files": 1719,
    "orphan_test_files": 15,
    "languages": ["csharp"]
  },
  "untested_sources": [
    {
      "path": "src/Foo/Bar.cs",
      "language": "csharp",
      "declaration_count": 8,
      "declarations": ["Bar", "BarOptions", "IBar", "..."],
      "suggested_test_path": "src/Foo/BarTests.cs"
    }
  ],
  "orphan_tests": [
    { "path": "tests/SomeIntegrationTest.cs", "language": "csharp" }
  ]
}
工作原理
  1. 文件发现 — 递归遍历目录,并跳过常见的构建/供应商目录(bin、 obj、node_modules、target、dist、build、vendor、__pycache__、 .venv、.git、……)和生成文件(.d.ts、.g.cs、.Designer.cs、 _pb2.py、*.min.js、AssemblyInfo.cs、……)。

  2. 语言检测 — detect_language_from_path 将扩展名映射到受支持的语言;跳过未知扩展名。

  3. 测试与源文件分类 — 各语言对应的路径启发式规则:

    语言测试规则
    Python路径包含 tests//test/;或文件名以 test_ 开头或以 _test.py 结尾;或文件名为 conftest.py。
    JS/TS/TSX路径包含 __tests__、tests、test、spec、e2e;或文件名包含 .test./.spec.。
    Go文件名以 _test.go 结尾。
    Java路径包含 test/tests;或文件名以 Test.java/Tests.java 结尾。
    Rust路径包含 tests//benches/。
    C#路径包含 tests/;或项目路径段以 .Tests/.Test/.UnitTests/.IntegrationTests 结尾;或文件名以 Tests/Test 结尾。
    Ruby路径包含 spec//test/;或文件名以 _spec.rb/_test.rb 结尾。
    Kotlin路径包含 test//tests//spec/;或文件名以 Test.kt/Tests.kt/Spec.kt 结尾。
    Swift路径包含 test//tests//uitests//integrationtests/(不区分大小写);或文件名以 Test.swift/Tests.swift 结尾。
    PowerShell路径包含 test//tests//pester/;或文件名以 .Tests.ps1/.Test.ps1 结尾。
    C++路径包含 test//tests//testing/;或文件名以 test_ 开头,或以 _test.cpp/_tests.cpp 结尾。
  4. 逐文件提取 — process(text, ProcessConfig(structure, imports, symbols)) 返回已声明项、原始导入语句以及扁平的已声明名称列表。

  5. 配对 — 对每个测试文件,将导入解析(按语言处理,例如 Python 的 from pkg.mod import x → pkg/mod.py;Java 的 import a.b.C; → a/b/C.java;C# 的 using 表示命名空间而非文件,因此不执行操作)与标识符重叠(类单词的 token,长度 ≥ 4,与已声明名称匹配)合并。

  6. 输出 JSON — untested_sources 按声明数量降序排列。

局限性(请如实告知 Agent)

两种引擎都是静态、仅解析的启发式方法;与覆盖率分析相比,它们以少量准确性为代价,大幅降低了成本。已知缺漏:

  • 反射/通过 DI 解析的类型:如果仅通过字符串名称或容器解析引用,则无法检测到;测试源代码中不会出现该类型的短名称。
  • 扩展方法:以实例方法形式调用时(C#),不会写出声明该方法的静态类名,因此不会将其文件计入。
  • var、目标类型推断的 new()、模式匹配:会丢失类型 token;文件级合并通常仍可通过其他引用捕获该文件。
  • 短标识符名称:为避免对 id、db、Tag 等名称产生噪声配对,会忽略多语言代码中的长度小于 4 的名称。
  • Monorepo 路径别名:不会解析 TS 路径映射、Java module-info;如果两个文件具有相同的路径后缀,后缀匹配回退可能会选错源文件。

遇到这些情况时,请对 Agent 已完成初步筛选、但尚未配对的候选项运行实际覆盖率分析(coverage-analysis)。

最终结果始终应标注为静态配对启发式方法,而非行覆盖率或分支覆盖率的证据。即使每个请求的源文件都有明显匹配或明显缺失的测试,也必须注明这一点。

Agent 应读取的输出

  • untested[*].source / untested_sources[*].path — 选择下一个要测试的源文件(按声明数量从高到低排列)。
  • *.suggested_test_path — 新测试文件的即插即用目标路径;Roslyn 引擎会使用已通过 <ProjectReference> 引用该源文件所在项目的测试项目,因此无需执行 dotnet sln add。如果找不到测试根目录,polyglot 引擎可能会建议将测试文件与源文件放在同一目录。若某个源文件的同目录文件已有对应测试,则该测试目录已成为约定目录;缺少对应测试的同目录文件必须沿用该目录,而不是退而选择与源文件放在同一目录。
  • source_to_tests(Roslyn)/ --include-tested tested_sources(polyglot)— 确认新编写的测试文件已列入预期源文件对应的列表。
  • orphan_tests(polyglot)— 未引用任何同语言源文件的测试;可用于排查过时测试或仅用于集成测试的测试。

评论

支持 Markdown