Test Maintainability Assessment
Analyze .NET test code for maintainability issues: duplicated boilerplate, copy-paste test methods, and structural repetition across test methods and classes. Produce a report of refactoring opportunities with concrete before/after suggestions. The goal is analysis only — do not modify any files.
When to Use
- User asks to find duplicated code or boilerplate in tests
- User wants to know where test code can be DRY-ed up
- User asks to reduce test duplication, improve test readability, or clean up test boilerplate
- User asks for refactoring opportunities in a test suite
- User wants to identify shared setup or teardown candidates
- User asks "what patterns repeat across my tests?"
- User wants to centralize test data, introduce builders or helpers
When Not to Use
- User wants to write new tests from scratch (use
writing-mstest-tests)
- User wants to detect anti-patterns or code smells (use
test-anti-patterns)
- User wants to actually perform the refactoring (help them directly, this skill only analyzes)
Inputs
| Input |
Required |
Description |
| Test code |
Yes |
One or more test files or a test project directory to analyze |
| Production code |
No |
The code under test, for context on what abstractions might help |
| Scope |
No |
Whether to analyze within a single class or across multiple classes |
Workflow
Step 1: Gather the test code
Read all test files the user provides or references. If the user points to a directory or project, scan for all test files using these framework markers:
| Framework |
Test class markers |
Test method markers |
| MSTest |
[TestClass] |
[TestMethod], [DataTestMethod] |
| xUnit |
(none — convention-based) |
[Fact], [Theory] |
| NUnit |
[TestFixture] |
[Test], [TestCase], [TestCaseSource] |
| TUnit |
(none — convention-based) |
[Test] |
Step 2: Identify maintainability issues
Scan for these categories:
Category 1: Repeated object construction
Look for the same object being constructed in 3+ test methods with identical or near-identical parameters.
Indicators:
new ClassName(...) appearing with identical arguments in multiple tests
- Multiple tests creating the same "system under test" with similar configuration
- Repeated mock/fake/stub creation with the same setup
Potential refactorings:
- Extract a factory method or test helper (e.g.,
CreateSut(), CreateDefaultOrder())
- Use
[TestInitialize]/constructor/[SetUp] for shared construction
- Introduce a builder pattern for complex objects with many variations
Example — before:
[TestMethod]
public void Process_ValidOrder_Succeeds()
{
var logger = new FakeLogger();
var email = new FakeEmailService();
var inventory = new FakeInventory(stock: 100);
var processor = new OrderProcessor(logger, email, inventory);
// ...
}
[TestMethod]
public void Process_EmptyItems_Fails()
{
var logger = new FakeLogger();
var email = new FakeEmailService();
var inventory = new FakeInventory(stock: 100);
var processor = new OrderProcessor(logger, email, inventory);
// ...
}
After — extract factory:
private static OrderProcessor CreateProcessor(int stock = 100)
{
return new OrderProcessor(new FakeLogger(), new FakeEmailService(), new FakeInventory(stock));
}
Category 2: Repeated assertion patterns
Look for the same sequence of assertions appearing in 3+ test methods.
Indicators:
- Multiple tests asserting the same set of properties on a result object
- Repeated null-check-then-value-check sequences
- Same collection of
Assert.AreEqual calls across methods
Potential refactorings:
- Extract a custom assertion helper (e.g.,
AssertValidOrder(order, expectedTotal, expectedStatus))
- Use framework-specific assertion extensions
- Introduce a
Verify method that checks a standard set of properties
Category 3: Copy-paste test methods
Look for test methods with near-identical bodies differing only in input values or a single parameter.
Indicators:
- 3+ methods with the same structure but different literal values
- Methods that could be collapsed into
[DataRow]/[Theory]/[TestCase]
- Test names that follow a pattern like
Method_Input1_Result, Method_Input2_Result
Potential refactorings:
- Convert to parameterized tests with
[DataRow]/[InlineData]/[TestCase]
- Use
[DynamicData]/[MemberData]/[TestCaseSource] for complex inputs
- Prefer
[DataRow] with DisplayName over [DynamicData] when all values are compile-time constants. Reserve [DynamicData] for computed or complex values.
- Add
DisplayName for non-obvious parameter values. [DataRow("Gold", 100.0, 90.0)] is self-explanatory; [DataRow(3, 7, 42)] is not.
Category 4: Duplicated setup/teardown logic
Look for initialization or cleanup code repeated across test classes.
Indicators:
- Multiple
[TestInitialize]/[SetUp] methods with similar bodies
- Repeated database seeding, file creation, or HTTP client configuration
- Same
using/IDisposable cleanup pattern across classes
Potential refactorings:
- Extract a shared test base class or fixture
- Use composition with a shared helper class
- Create a test context factory
Category 5: Repeated test infrastructure
Look for structural patterns shared across test classes.
Indicators:
- Same mock interfaces configured identically in multiple classes
- Repeated
HttpClient setup with similar DelegatingHandler patterns
- Same logging/configuration scaffolding across test classes
Potential refactorings:
- Extract a shared test fixture or helper library
- Create reusable fake implementations
- Introduce a test harness class
Step 3: Apply calibration rules
Before reporting, filter findings through these rules:
- Only report at 3+ occurrences. Two similar setups are not boilerplate — they may be intentional clarity.
- Don't flag simple constructors.
new Calculator() or new List<int>() is not meaningful boilerplate. Don't recommend builders for new User(1, "Alice") either.
- Respect intentional verbosity. If each test is self-contained and reads clearly on its own, explicit setup per test is a valid choice. Note it but don't flag it as a problem.
- Distinguish structural similarity from true duplication. Tests that follow AAA (Arrange-Act-Assert) will look similar by nature. Only flag when the actual code (not just the structure) is duplicated.
- Consider the blast radius of refactoring. A helper shared across 20 tests creates coupling. Note the trade-off.
- If tests are already well-maintained, say so. A report finding only minor opportunities is perfectly valid. Acknowledge what's already good.
Step 4: Report findings
Present findings in this structure:
- Summary — How many patterns found, broken down by category. If the test suite is clean, lead with that.
- Findings by category — For each pattern found:
- Category name and description
- Locations: list the specific test methods and files involved
- The duplicated code pattern (show a representative sample)
- Suggested refactoring with a concrete before/after example
- Estimated impact: how many lines/methods would be simplified
- Refactoring priority — Rank findings by:
- Occurrence count (more occurrences = higher value)
- Complexity of the duplicated code (complex setup > simple construction)
- Risk (low-risk extractions first)
- Trade-offs — For each suggestion, note:
- What readability is gained
- What locality/independence is lost
- Whether it's worth it given the occurrence count
Validation
Common Pitfalls
| Pitfall |
Solution |
| Flagging AAA structure as duplication |
The Arrange-Act-Assert pattern is not boilerplate — flag only when the actual code repeats |
| Suggesting extraction for 2 occurrences |
Wait for 3+ before recommending extraction |
| Recommending base classes for everything |
Prefer composition (helpers, factories) over inheritance |
| Ignoring the readability cost |
Every extraction adds indirection — note the trade-off |
Flagging simple new X() as boilerplate |
Only flag complex construction with multiple parameters or configuration |
| Recommending DRY at the expense of test isolation |
Tests that share mutable state through helpers become coupled — warn about this |
1---2name: exp-test-maintainability3description: Detects duplicate boilerplate, copy-paste tests, and structural maintainability issues across .NET test suites. Use when the user asks to reduce repetition, consolidate similar test methods, convert copy-paste tests to data-driven parameterized tests, suggest a better test structure, or identify refactoring opportunities. Identifies repeated construction, assertion patterns, copy-paste methods convertible to DataRow/Theory/TestCase, redundant setup/teardown, and shared infrastructure. Produces an analysis report with concrete before/after suggestions. Works with MSTest, xUnit, NUnit, and TUnit. DO NOT USE FOR: writing new tests (use writing-mstest-tests), reviewing test quality or anti-patterns (use test-anti-patterns), or deep mock auditing (use exp-mock-usage-analysis).4license: MIT5---67# Test Maintainability Assessment89Analyze .NET test code for maintainability issues: duplicated boilerplate, copy-paste test methods, and structural repetition across test methods and classes. Produce a report of refactoring opportunities with concrete before/after suggestions. The goal is analysis only — do not modify any files.1011## When to Use1213- User asks to find duplicated code or boilerplate in tests14- User wants to know where test code can be DRY-ed up15- User asks to reduce test duplication, improve test readability, or clean up test boilerplate16- User asks for refactoring opportunities in a test suite17- User wants to identify shared setup or teardown candidates18- User asks "what patterns repeat across my tests?"19- User wants to centralize test data, introduce builders or helpers2021## When Not to Use2223- User wants to write new tests from scratch (use `writing-mstest-tests`)24- User wants to detect anti-patterns or code smells (use `test-anti-patterns`)25- User wants to actually perform the refactoring (help them directly, this skill only analyzes)2627## Inputs2829| Input | Required | Description |30|-------|----------|-------------|31| Test code | Yes | One or more test files or a test project directory to analyze |32| Production code | No | The code under test, for context on what abstractions might help |33| Scope | No | Whether to analyze within a single class or across multiple classes |3435## Workflow3637### Step 1: Gather the test code3839Read all test files the user provides or references. If the user points to a directory or project, scan for all test files using these framework markers:4041| Framework | Test class markers | Test method markers |42|-----------|--------------------|---------------------|43| MSTest | `[TestClass]` | `[TestMethod]`, `[DataTestMethod]` |44| xUnit | *(none — convention-based)* | `[Fact]`, `[Theory]` |45| NUnit | `[TestFixture]` | `[Test]`, `[TestCase]`, `[TestCaseSource]` |46| TUnit | *(none — convention-based)* | `[Test]` |4748### Step 2: Identify maintainability issues4950Scan for these categories:5152#### Category 1: Repeated object construction5354Look for the same object being constructed in 3+ test methods with identical or near-identical parameters.5556**Indicators:**57- `new ClassName(...)` appearing with identical arguments in multiple tests58- Multiple tests creating the same "system under test" with similar configuration59- Repeated mock/fake/stub creation with the same setup6061**Potential refactorings:**62- Extract a factory method or test helper (e.g., `CreateSut()`, `CreateDefaultOrder()`)63- Use `[TestInitialize]`/constructor/`[SetUp]` for shared construction64- Introduce a builder pattern for complex objects with many variations6566**Example — before:**67```csharp68[TestMethod]69public void Process_ValidOrder_Succeeds()70{71 var logger = new FakeLogger();72 var email = new FakeEmailService();73 var inventory = new FakeInventory(stock: 100);74 var processor = new OrderProcessor(logger, email, inventory);75 // ...76}7778[TestMethod]79public void Process_EmptyItems_Fails()80{81 var logger = new FakeLogger();82 var email = new FakeEmailService();83 var inventory = new FakeInventory(stock: 100);84 var processor = new OrderProcessor(logger, email, inventory);85 // ...86}87```8889**After — extract factory:**90```csharp91private static OrderProcessor CreateProcessor(int stock = 100)92{93 return new OrderProcessor(new FakeLogger(), new FakeEmailService(), new FakeInventory(stock));94}95```9697#### Category 2: Repeated assertion patterns9899Look for the same sequence of assertions appearing in 3+ test methods.100101**Indicators:**102- Multiple tests asserting the same set of properties on a result object103- Repeated null-check-then-value-check sequences104- Same collection of `Assert.AreEqual` calls across methods105106**Potential refactorings:**107- Extract a custom assertion helper (e.g., `AssertValidOrder(order, expectedTotal, expectedStatus)`)108- Use framework-specific assertion extensions109- Introduce a `Verify` method that checks a standard set of properties110111#### Category 3: Copy-paste test methods112113Look for test methods with near-identical bodies differing only in input values or a single parameter.114115**Indicators:**116- 3+ methods with the same structure but different literal values117- Methods that could be collapsed into `[DataRow]`/`[Theory]`/`[TestCase]`118- Test names that follow a pattern like `Method_Input1_Result`, `Method_Input2_Result`119120**Potential refactorings:**121- Convert to parameterized tests with `[DataRow]`/`[InlineData]`/`[TestCase]`122- Use `[DynamicData]`/`[MemberData]`/`[TestCaseSource]` for complex inputs123- Prefer `[DataRow]` with `DisplayName` over `[DynamicData]` when all values are compile-time constants. Reserve `[DynamicData]` for computed or complex values.124- Add `DisplayName` for non-obvious parameter values. `[DataRow("Gold", 100.0, 90.0)]` is self-explanatory; `[DataRow(3, 7, 42)]` is not.125126#### Category 4: Duplicated setup/teardown logic127128Look for initialization or cleanup code repeated across test classes.129130**Indicators:**131- Multiple `[TestInitialize]`/`[SetUp]` methods with similar bodies132- Repeated database seeding, file creation, or HTTP client configuration133- Same `using`/`IDisposable` cleanup pattern across classes134135**Potential refactorings:**136- Extract a shared test base class or fixture137- Use composition with a shared helper class138- Create a test context factory139140#### Category 5: Repeated test infrastructure141142Look for structural patterns shared across test classes.143144**Indicators:**145- Same mock interfaces configured identically in multiple classes146- Repeated `HttpClient` setup with similar `DelegatingHandler` patterns147- Same logging/configuration scaffolding across test classes148149**Potential refactorings:**150- Extract a shared test fixture or helper library151- Create reusable fake implementations152- Introduce a test harness class153154### Step 3: Apply calibration rules155156Before reporting, filter findings through these rules:157158- **Only report at 3+ occurrences.** Two similar setups are not boilerplate — they may be intentional clarity.159- **Don't flag simple constructors.** `new Calculator()` or `new List<int>()` is not meaningful boilerplate. Don't recommend builders for `new User(1, "Alice")` either.160- **Respect intentional verbosity.** If each test is self-contained and reads clearly on its own, explicit setup per test is a valid choice. Note it but don't flag it as a problem.161- **Distinguish structural similarity from true duplication.** Tests that follow AAA (Arrange-Act-Assert) will look similar by nature. Only flag when the actual code (not just the structure) is duplicated.162- **Consider the blast radius of refactoring.** A helper shared across 20 tests creates coupling. Note the trade-off.163- **If tests are already well-maintained, say so.** A report finding only minor opportunities is perfectly valid. Acknowledge what's already good.164165### Step 4: Report findings166167Present findings in this structure:1681691. **Summary** — How many patterns found, broken down by category. If the test suite is clean, lead with that.1702. **Findings by category** — For each pattern found:171 - Category name and description172 - Locations: list the specific test methods and files involved173 - The duplicated code pattern (show a representative sample)174 - Suggested refactoring with a concrete before/after example175 - Estimated impact: how many lines/methods would be simplified1763. **Refactoring priority** — Rank findings by:177 - Occurrence count (more occurrences = higher value)178 - Complexity of the duplicated code (complex setup > simple construction)179 - Risk (low-risk extractions first)1804. **Trade-offs** — For each suggestion, note:181 - What readability is gained182 - What locality/independence is lost183 - Whether it's worth it given the occurrence count184185## Validation186187- [ ] Every finding includes specific file and method locations188- [ ] Every finding shows the actual duplicated code, not just a description189- [ ] Every suggestion includes a concrete before/after example190- [ ] Findings are filtered through the 3+ occurrence threshold191- [ ] Simple constructors are not flagged192- [ ] Trade-offs are acknowledged for each suggestion193- [ ] If tests are clean, the report says so upfront194195## Common Pitfalls196197| Pitfall | Solution |198|---------|----------|199| Flagging AAA structure as duplication | The Arrange-Act-Assert pattern is not boilerplate — flag only when the actual code repeats |200| Suggesting extraction for 2 occurrences | Wait for 3+ before recommending extraction |201| Recommending base classes for everything | Prefer composition (helpers, factories) over inheritance |202| Ignoring the readability cost | Every extraction adds indirection — note the trade-off |203| Flagging simple `new X()` as boilerplate | Only flag complex construction with multiple parameters or configuration |204| Recommending DRY at the expense of test isolation | Tests that share mutable state through helpers become coupled — warn about this |