Raw capture โ raw/litmus-dotnet-testing-priority.md
url: https://dev.to/ebrahimsayed/you-inherited-a-net-codebase-with-zero-tests-now-what-46ca title: "You inherited a .NET codebase with zero tests. Now what?" author: Ebrahim Sayed Ebrahim date_fetched: 2026-07-18 date_published: 2025-03-27
Every .NET developer eventually faces a codebase with no tests โ an empty test project or one with "three tests from 2019 that no longer compile." The challenge: where to begin?
The author identifies two common but inadequate approaches:
Strategy A โ Alphabetical order: Testing files in sequence regardless of actual risk. The author notes this "feels like progress but doesn't address the actual risk," leading to outdated files getting coverage while high-churn files stay unprotected.
Strategy B โ Reactive testing: Writing tests only for bugs just fixed. The author calls this "not a strategy โ it's a reflex."
The author references Roy Osherove's The Art of Unit Testing, which recommends ranking files by risk and testability. But since manual scoring across hundreds of files isn't practical, the author automated it.
Litmus is a .NET CLI tool that ranks files by testing priority. Setup requires two commands:
dotnet tool install --global dotnet-litmus
dotnet-litmus scan
No server, dashboard, or config file needed. It outputs files grouped into three buckets: Act Now, Next Sprint, and Monitor โ ranked by commits, coverage, complexity, coupling, risk, and priority.
Existing tools like SonarQube and coverage platforms don't evaluate whether a file is testable right now or needs refactoring first. The author draws on Michael Feathers' concept of "seams" from Working Effectively with Legacy Code โ points where you can substitute dependencies during testing without modifying production code.
Litmus uses Roslyn to scan for six categories of "unseamed dependencies":
DateTime.Now, File.ReadAllText(), raw new HttpClient()OrderService instead of IOrderServiceawait _httpClient.GetAsync(), await _db.SaveChangesAsync()(ConcreteType)expr that bypasses interface abstractionsEach signal has its own weight. Infrastructure calls (2.0ร) are weighted most heavily because "there's literally no substitution point for DateTime.Now."
Phase 1 โ Risk: RiskScore = Churn ร (1 - Coverage) ร (1 + Complexity). Frequently changed files with low coverage and high complexity score highest.
Phase 2 โ Starting Priority: StartingPriority = RiskScore ร (1 - Coupling). Risk is discounted based on how entangled the file is. A decoupled file keeps its full score; a maximally entangled one drops to zero priority โ not because it's safe, but because testing it first would waste the sprint.
The three output buckets derive from this: Act Now (high risk, low coupling), Next Sprint (high risk, high coupling), and Monitor (lower risk or too entangled).
The --no-coverage flag skips test runs entirely for codebases starting from zero. Files are still ranked by churn, complexity, and coupling.
The --detailed flag shows method-level data within files, revealing, for example, that ValidateInput has "zero coverage and non-trivial complexity" while ProcessOrder is partially covered. Users can export baselines with --output baseline.json and compare later with --baseline baseline.json, generating a Delta column for sprint retrospectives.
Litmus works in GitHub Actions with fetch-depth: 0 (required for full git history). A --fail-on-threshold flag acts as a quality gate โ any file exceeding the risk threshold fails the build.
The author positions them as complementary: "SonarQube is a broad code quality monitoring platform" requiring a server, while Litmus answers one specific question about where to start testing, runs without a server, and is free.
The author explains building the tool after repeatedly performing the same manual exercise โ "scanning git logs, cross-referencing with coverage reports, eyeballing the source for testability" โ across engagements. The scoring model is intentionally transparent: "Every score is reproducible."
dotnet-litmusFetched from DEV Community on 2026-07-18