Using Agents to Backfill NUnit Tests on a 2012 C# Monolith
AI & Agents 14 April 2026 · 7 min

Using Agents to Backfill NUnit Tests on a 2012 C# Monolith

By wGrow Project Team ·

The Refactoring Deadlock in Legacy Microsoft Stacks

IT professional inspecting server rack cabling in a data center.

A decade-old .NET 4.5 Web Forms portal can run revenue-critical billing and pricing workflows — live, in production, with no downtime tolerance. Zero unit tests. The business wants it rebuilt as an ASP.NET Core application — billing logic and all — and the sound response is to refuse to touch the architecture until there is a baseline. That refusal costs an awkward conversation. It avoids a long regression nightmare.

Refactoring old code without a test harness is professional negligence. Writing that harness manually across a decade-old monolith is commercially unsustainable. A legacy Microsoft stack is a specific archaeology problem: business logic bleeds into code-behind files, global state gets mutated in a dozen places you haven’t found yet, DbContext instances are newed up directly inside button-click handlers. There is no clean seam to cut.

The standard playbook: spend three sprints reading the codebase, write characterisation tests, then begin the refactor. On a large portal, three sprints stretch fast. Engineers fatigue reading spaghetti they didn’t write. Coverage stays thin. The refactor begins anyway because the client is paying. Regressions appear. Everyone blames the refactor when the real culprit was the untested baseline.

A dedicated AI agent crew can read the legacy C# and generate baseline NUnit test coverage before anyone touches the architecture. The rest of this piece explains how such a crew can be structured, where it works, and where it fails.


LLMs Make Better Archaeologists Than Architects

Here’s the honest position on current AI coding tools: they tend to struggle with coherent net-new architecture in domains they haven’t seen concretely specified. Ask an LLM to greenfield a complex multi-tenant platform and you’ll likely get something structurally plausible that buckles under the first real domain constraint. Ask the same model to read a 500-line method — eight nested conditionals, twelve if/else branches, side effects on three static fields — and it produces a readable execution trace.

That asymmetry is the whole point.

Point agents at specific unmapped assemblies, one at a time. The agent’s job is not to critique the code. It is to trace execution paths and map inputs to outputs — the same work a human engineer would do with a pad and a whiteboard, but without the mounting existential dread, and in a fraction of the time.

The prompting strategy should be deliberate and tightly constrained: lock in current behaviour. Bugs included. A baseline characterisation test is not a correctness assertion. It’s a regression tripwire. If the legacy method returns a wrong freight surcharge for a particular lane configuration, the baseline test asserts that wrong answer — but only until the bug is deliberately fixed. When the fix lands, you update or retire that specific characterisation test in the same change. The safety net is for catching accidental drift; it is not a mechanism for freezing known defects indefinitely.

This distinction matters more than it sounds. Engineers used to writing meaningful tests instinctively resist asserting incorrect output. The agent has no such instinct — it follows the instruction. For this specific task, that is an advantage.

First-pass human tracing of a roughly 500-line method is slow, while an agent can produce the initial execution-path map in a fraction of the time. Senior-engineer review is still needed; it moves the work from first-pass tracing to verification. Across an assembly with dozens of such methods, that shift in effort profile compounds quickly.


Automating the Tedium of Tightly Coupled Dependencies

Engineer reviewing legacy C# dependency graphs on a monitor in an office meeting room.

Legacy C# Pattern
1 public OrderInfo GetOrder(int id) {
2 using (var db = new LogisticsEntities()) { ← ①
3 return db.Orders.Find(id);
4 }
5 }
  1. ① Direct Entity Framework context isolated by agent using Microsoft Fakes

The primary bottleneck in legacy test generation is not test logic. It’s dependency isolation. Old systems don’t use dependency injection. Dependencies are instantiated inline, resolved through service locators, or pulled from static singletons — never injected through a clean interface. There’s no boundary to mock against.

A portal built on Entity Framework 5 can have a single massive DbContext subclass: dozens of DbSet properties, a sprawl of navigation properties, direct instantiation inside service methods that were themselves called from code-behind files. No repository layer. No unit-of-work abstraction. One enormous context that knows everything about everything.

Writing a test for any method touching that context requires either spinning up a real database — slow, fragile, and not a unit test by any reasonable definition — or building a mock that replicated enough of EF’s behaviour to fool the method under test.

An agent crew can handle this in two modes depending on how much you’re willing to touch production code. Where a minimal interface wrapper is feasible without modifying production paths, the agents can generate the wrapper and the corresponding mock. Where the coupling is too severe — production code constructs the context directly with new and no seam exists — they can use Microsoft Fakes shims to intercept the constructor call. Where a seam is already present, or a thin wrapper can be introduced without altering runtime behaviour, Moq-based DbContext substitutes built on EF5-specific faking patterns handle the isolation instead.

Concretely, agents can generate the full boilerplate mock setup for a large context automatically — work that would otherwise mean an engineer parsing the EF model and wiring up the Moq configuration by hand. The engineer’s job becomes verification, not authorship.

The limitation is real, and you need to know it. The agent can produce a mock setup that violates the constraints of the legacy framework. EF5 mocking patterns differ from EF6 and EF Core in specific, non-obvious ways. A model trained predominantly on modern stack examples can generate a DbSet mock that compiles but fails at runtime because it doesn’t correctly replicate EF5’s change-tracking behaviour. These failures can produce false confidence — a green test that is not actually exercising the production code path. This is not a failure mode you catch only in QA.


The Mandatory Human Review Gate

Verification Flow
Test Generation
Dependency Mocking
Compilation
Logic Verification
Agent Crew
Trace paths
Configure Moq/Fakes
Senior Engineer
Pull & build branch
Review & observe pass

The same agentic pattern applies to internal technical debt, such as a legacy IIS HTTP module that intercepts and transforms XML-based API calls. Scoping the work to the core transformation and routing assemblies keeps it tractable ahead of an ASP.NET Core middleware migration.

The review gate is non-negotiable. Agents write the test and configure the mock. Agents do not commit to the main branch. Every generated test goes into a feature branch. A human engineer pulls the branch, compiles the test project, and watches it pass locally. No exceptions — including when you’re behind schedule.

On a module like that, reviewing and correcting agent-generated tests can take far less effort than writing the suite manually. The engineer shifts from writing boilerplate to operating as a code reviewer. That’s a different cognitive mode, and a significantly less exhausting one.

Three failure modes that human reviewers must be trained to catch:

Tests that compile but test nothing. The classic empty assert, or an assertion on a value the mock itself returns without the production code being exercised. These pass every time and protect against nothing.

Mocks that bypass the actual business logic. A mock configured to return a specific value for the method under test, rather than for a dependency of that method. The test becomes a tautology.

Assertion granularity too coarse to be meaningful. An agent asserting that a method returns “not null” when the real business rule is a specific freight calculation. Passes. Tests nothing of value.

These are predictable output patterns when agents receive underspecified prompts or insufficiently constrained instructions. The fix is in the prompting and the review checklist — not in the model’s underlying capability.


Shifting Engineering Cycles to Actual Modernisation

Two engineers reviewing software on dual monitors at a desk.

LLMs will not automatically rewrite a legacy enterprise system cleanly. Anyone selling that outcome is running well ahead of the current evidence. What they can do is handle the archaeological grunt work that lets human engineers execute modernisation safely.

The ROI here is not abstract. Fewer billable hours on boilerplate test generation. A meaningful reduction in developer burnout from repetitive scaffolding. A reliable NUnit baseline established faster than purely manual approaches allow. Baseline coverage on the core calculation assemblies can arrive in a fraction of the elapsed time a purely manual effort would need.

That gap is the modernisation window. Senior engineers who would otherwise spend weeks writing DbContext mocks are instead reviewing agent output and executing the actual .NET Core migration. The refactor begins from a position of evidence, not hope.

Two things have to hold for the approach to deliver real savings. First, prompting discipline: instruct the agent to characterise, not improve; give it one assembly at a time; require it to enumerate every external dependency before generating a single test. Second, a review gate staffed by someone who knows what an empty assert looks like and has read the framework version constraints.

Neither condition is onerous. Both are necessary.

Stop asking senior developers to reverse-engineer decade-old C# by hand. Deploy an agent crew to build the safety net. Then execute the refactor.