DotNetTestRadar 0.1.0
dotnet tool install --global DotNetTestRadar --version 0.1.0
dotnet new tool-manifest
dotnet tool install --local DotNetTestRadar --version 0.1.0
#tool dotnet:?package=DotNetTestRadar&version=0.1.0
nuke :add-package DotNetTestRadar --version 0.1.0
DotNetTestRadar
A .NET global CLI tool that answers two complementary questions when adding tests to a legacy codebase:
- Where is it dangerous to leave code untested? — ranked by Risk Score
- Where can you actually start testing today? — ranked by Starting Priority
It cross-references four signals to produce both scores:
- Git churn — how frequently a file changes
- Code coverage — how well a file is tested
- Cyclomatic complexity — how complex the file's logic is
- Dependency entanglement — how many unseamed dependencies the file has (seam analysis)
The result is a ranked table sorted by Starting Priority: files that are both dangerous and practically testable today appear at the top. Files that are dangerous but heavily entangled appear lower, with a clear signal to introduce seams before attempting to test them.
Quick Start
Prerequisites
- .NET 8 SDK or later
- git installed and available on PATH
- A Cobertura XML coverage report (produced by
coverlet,dotnet-coverage, ReportGenerator, etc.)
Install
# Pack the tool
dotnet pack DotNetTestRadar/DotNetTestRadar.csproj -c Release
# Install as a global tool from the local package
dotnet tool install --global --add-source DotNetTestRadar/bin/Release DotNetTestRadar
# Or install from nuget.org (after publishing)
dotnet tool install --global DotNetTestRadar
# Or run directly without installing
dotnet run --project DotNetTestRadar -- analyze --solution path/to/YourApp.sln --coverage coverage.xml
Two ways to run
Option A — One step with scan (recommended for getting started):
dotnet-testradar scan --solution MyApp.sln
This runs dotnet test, collects coverage automatically, then runs the full analysis. No need to generate a coverage file first.
Option B — Two steps with analyze (when you already have coverage):
# Generate coverage separately (e.g. in CI)
dotnet test --collect:"XPlat Code Coverage"
# Then analyze
dotnet-testradar analyze --solution MyApp.sln --coverage TestResults/.../coverage.cobertura.xml
What scan does
- Runs
dotnet testwith the XPlat Code Coverage collector (ordotnet-coverageif--coverage-tool dotnet-coverageis specified) - Streams live output so you can see build progress and test results in real time
- Discovers all
coverage.cobertura.xmlfiles produced (one per test project) - Merges them (taking the highest coverage rate per source file)
- Runs the full analysis pipeline (git churn → complexity → seam detection → scoring)
- Cleans up the temporary test results directory
CLI Options
scan — runs tests and analyzes in one step
| Option | Required | Default | Description |
|---|---|---|---|
--solution |
Yes | -- | Path to a .sln or .slnx file |
--tests-dir |
No | solution file | Directory or project to run dotnet test against |
--coverage-tool |
No | coverlet | Coverage collector: coverlet or dotnet-coverage |
--timeout |
No | 10 | Maximum minutes to wait for test execution |
--since |
No | 1 year ago | Limit git history to commits after this date (ISO format) |
--top |
No | 20 | Number of top files to display |
--exclude |
No | -- | Glob pattern(s) to exclude files (repeatable) |
--output |
No | -- | Export full results to a .json or .csv file |
--baseline |
No | -- | Path to a previous JSON export to compare against |
--format |
No | table | Output format for stdout: table, json, or csv |
--verbose |
No | false | Show detailed intermediate scores and live test output |
--quiet |
No | false | Suppress all output except errors (exit code only) |
--no-color |
No | false | Disable colored output |
analyze — use an existing coverage file
| Option | Required | Default | Description |
|---|---|---|---|
--solution |
Yes | -- | Path to a .sln or .slnx file |
--coverage |
Yes | -- | Path to a Cobertura XML coverage file |
--since |
No | 1 year ago | Limit git history to commits after this date (ISO format) |
--top |
No | 20 | Number of top files to display |
--exclude |
No | -- | Glob pattern(s) to exclude files (repeatable) |
--output |
No | -- | Export full results to a .json or .csv file |
--baseline |
No | -- | Path to a previous JSON export to compare against |
--format |
No | table | Output format for stdout: table, json, or csv |
--verbose |
No | false | Show detailed intermediate scores for each file |
--quiet |
No | false | Suppress all output except errors (exit code only) |
--no-color |
No | false | Disable colored output |
Examples
Quickest start — run tests and analyze in one command
dotnet-testradar scan --solution MyApp.sln
Scan with a specific test directory, export to JSON
dotnet-testradar scan \
--solution MyApp.sln \
--tests-dir tests/MyApp.Tests \
--output report.json
Scan the last 6 months, show top 10
dotnet-testradar scan \
--solution src/MyApp.sln \
--since 2025-08-01 \
--top 10
Use dotnet-coverage instead of coverlet
If scan hangs or times out due to coverlet issues, use Microsoft's dotnet-coverage tool instead:
# Install dotnet-coverage first (one-time)
dotnet tool install --global dotnet-coverage
# Then run scan with --coverage-tool
dotnet-testradar scan \
--solution MyApp.sln \
--coverage-tool dotnet-coverage
Increase timeout for large solutions
dotnet-testradar scan \
--solution MyApp.sln \
--timeout 30
Analyze with an existing coverage file, exclude generated code
dotnet-testradar analyze \
--solution MyApp.sln \
--coverage coverage.xml \
--exclude "*.Generated.cs" \
--exclude "**/ViewModels/*.cs" \
--output report.json
Compare against a baseline (CI diff mode)
# First run: save a baseline
dotnet-testradar analyze \
--solution MyApp.sln \
--coverage coverage.xml \
--output baseline.json
# Later: compare current state against the baseline
dotnet-testradar analyze \
--solution MyApp.sln \
--coverage coverage.xml \
--baseline baseline.json
When --baseline is provided, a Delta column appears in the table showing how each file's Starting Priority changed (+0.15 = degraded, -0.10 = improved, NEW = not in baseline). A summary line reports: vs baseline: N improved, N degraded, N new, N removed.
Pipe JSON to jq or other tools
# Get the top 5 files as JSON and filter with jq
dotnet-testradar analyze \
--solution MyApp.sln \
--coverage coverage.xml \
--format json | jq '.[].file'
# Export CSV to stdout for further processing
dotnet-testradar analyze \
--solution MyApp.sln \
--coverage coverage.xml \
--format csv > results.csv
Pipe-friendly plain output
dotnet-testradar analyze \
--solution MyApp.sln \
--coverage coverage.xml \
--no-color \
--output results.csv
Understanding the Output
The tool produces a table like this:
╭──────┬────────────────────────────────┬─────────┬──────────┬────────────┬────────────┬──────┬──────────┬────────╮
│ Rank │ File │ Commits │ Coverage │ Complexity │ Dependency │ Risk │ Priority │ Level │
├──────┼────────────────────────────────┼─────────┼──────────┼────────────┼────────────┼──────┼──────────┼────────┤
│ 1 │ Services/OrderService.cs │ 47 │ 12% │ 94 │ Low │ 1.42 │ 1.42 │ High │
│ 2 │ Services/ReportFormatter.cs │ 22 │ 31% │ 67 │ Low │ 0.71 │ 0.71 │ High │
│ 3 │ Controllers/PaymentGateway.cs │ 31 │ 8% │ 118 │ Very High │ 1.61 │ 0.32 │ Medium │
│ 4 │ Data/LegacyDbSync.cs │ 41 │ 0% │ 201 │ Very High │ 1.89 │ 0.19 │ Low │
╰──────┴────────────────────────────────┴─────────┴──────────┴────────────┴────────────┴──────┴──────────┴────────╯
4 files analyzed. 2 high-priority (start today), 1 medium-priority (next sprint). 2 high-risk file(s) need seam introduction before testing.
Reading the table:
- Dependency — cost of adding seams:
Low|Medium|High|Very High - Risk — how dangerous it is to leave untested (Phase 1 score, range 0–2.0)
- Priority — where to start today (Phase 2 score, range 0–2.0)
- Level — actionable tier based on Starting Priority
PaymentGateway.cs has a higher Risk score than OrderService.cs, but its Very High dependency level pushes its Starting Priority down to Medium. The tool is telling you: "This file is dangerous, but introduce seams before attempting to test it."
LegacyDbSync.cs is the most dangerous file in the codebase — but its Starting Priority is Low because it is too entangled to test directly today.
Color coding:
| Row Color | Meaning |
|---|---|
| Green | High priority — risky and testable now |
| Yellow | Medium priority — plan for next sprint |
| Default | Low priority — backlog or too entangled |
The Risk column is independently highlighted in red/yellow when a file is high/medium risk regardless of its starting priority. This makes it easy to spot the "introduce seams first" files.
Priority and Risk Levels
Starting Priority (primary output — sort order)
| Level | Score Range | Meaning |
|---|---|---|
| High | ≥ 0.6 | Start here — risky and practically testable now |
| Medium | ≥ 0.2 | Plan for the next sprint |
| Low | < 0.2 | Backlog — too costly relative to risk, or low risk overall |
Risk Level (secondary output)
| Level | Score Range | Meaning |
|---|---|---|
| High | ≥ 0.6 | Changes often, poorly tested, complex logic |
| Medium | ≥ 0.2 | Moderate risk — worth investigating |
| Low | < 0.2 | Low churn, well-tested, or simple code |
How Scores Are Calculated
Phase 1 — Risk Score
RiskScore = ChurnNorm × (1 - CoverageRate) × (1 + ComplexityNorm)
Each factor is normalized to [0, 1]:
- ChurnNorm: weighted lines changed relative to the most-changed file
- CoverageRate: line coverage from the Cobertura report (0.0 to 1.0)
- ComplexityNorm: cyclomatic complexity relative to the most complex file
Range: 0 to 2.0. A file that changes constantly, has no tests, and is highly complex scores near 2.0.
Phase 2 — Dependency Score and Starting Priority
The dependency score measures how many unseamed dependencies a file has — things a test cannot substitute, control, or observe. Six signals are detected using Roslyn syntax analysis:
| Signal | Weight | What it detects |
|---|---|---|
| Unseamed infrastructure calls | 2.0 | DateTime.Now, File.*, Environment.*, Guid.NewGuid(), new HttpClient(), new SomeDbContext() |
| Direct instantiation in methods | 1.5 | new ConcreteType() inside a method body (excluding DTOs, exceptions, collections) |
| Concrete constructor parameters | 0.5 | Constructor parameters that do not follow the ITypeName interface convention |
| Static calls on non-utility types | 1.0 | MyHelper.Transform(), CacheManager.Invalidate() (excluding Math, Convert, Enumerable, etc.) |
| Async seam calls | 1.5 | await _httpClient.GetAsync(), await _db.SaveChangesAsync(), and other known async I/O methods |
| Concrete downcasts | 1.0 | (ConcreteType)expr and expr as ConcreteType — defeats interface abstractions |
DI registration files (Program.cs, Startup.cs, files calling AddScoped/AddSingleton/AddTransient) are automatically detected and given a zeroed dependency score, since their high coupling is expected.
RawDependencyScore = (InfrastructureCalls × 2.0) + (DirectInstantiations × 1.5)
+ (ConcreteConstructorParams × 0.5) + (StaticCalls × 1.0)
+ (AsyncSeamCalls × 1.5) + (ConcreteCasts × 1.0)
DependencyNorm = RawDependencyScore / Max(RawDependencyScore across all files)
StartingPriority = RiskScore × (1 - DependencyNorm)
DependencyNorm = 0 (fully seamed) → StartingPriority = RiskScore.
DependencyNorm = 1 (maximally entangled) → StartingPriority = 0.
The two scores are kept separate intentionally: a file can be High Risk but Low Starting Priority. That combination is one of the most valuable signals in the output — it means "this file is dangerous but needs seam introduction before you can test it."
Default Exclusions
The following patterns are always excluded to reduce noise from auto-generated files:
*.Designer.cs*.g.cs/*.g.i.cs*Migrations/*.cs*AssemblyInfo.cs*.xaml.cs
Use --exclude to add additional patterns on top of these defaults.
Coverage Prerequisites (for analyze)
The scan command handles coverage automatically. If you use analyze and need to generate a coverage file manually:
dotnet test --collect:"XPlat Code Coverage"
This creates a coverage.cobertura.xml file inside TestResults/. For multiple test projects, use ReportGenerator to merge:
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"merged" -reporttypes:Cobertura
dotnet-testradar analyze --solution MyApp.sln --coverage merged/Cobertura.xml
The scan command does this merge automatically — it is generally the simpler option.
Solution Format Support
The tool supports both classic .sln files and the newer .slnx XML-based format. It automatically detects the format based on the file extension.
Troubleshooting
scan hangs during test execution
The most common cause is coverlet's data collector process hanging after tests complete. This is a known issue with certain versions of coverlet.collector and .NET SDK combinations.
Solutions (in order of preference):
Use
dotnet-coverageinstead: This avoids the coverlet data collector entirely.dotnet tool install --global dotnet-coverage dotnet-testradar scan --solution MyApp.sln --coverage-tool dotnet-coverageUpgrade coverlet: Update
coverlet.collectorin your test projects to the latest version.dotnet add <test-project> package coverlet.collectorIncrease the timeout: If coverage just takes a long time (large solution), increase the default 10-minute limit.
dotnet-testradar scan --solution MyApp.sln --timeout 30Use
analyzeinstead: Generate coverage separately and pass the file directly.dotnet-coverage collect "dotnet test MyApp.sln" -f cobertura -o coverage.xml dotnet-testradar analyze --solution MyApp.sln --coverage coverage.xml
Requirements
- .NET 8.0 or later
- Git must be installed and the solution must reside inside a git repository
scan: requires dotnet SDK withcoverlet.collectorin test projects (ordotnet-coverageinstalled globally when using--coverage-tool dotnet-coverage)analyze: requires a pre-generated Cobertura XML coverage report
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 137 | 3/1/2026 |
| 0.0.0-alpha.0.12 | 60 | 2/27/2026 |
| 0.0.0-alpha.0.11 | 71 | 2/27/2026 |
| 0.0.0-alpha.0.10 | 69 | 2/27/2026 |
| 0.0.0-alpha.0.9 | 70 | 2/25/2026 |
| 0.0.0-alpha.0.8 | 62 | 2/25/2026 |