Skip to content
Back to skills

Test Refactoring Catalog

ASecurity

Use when the tests pass but the test code itself has gone bad — the same setup copied into several tests, names that describe the method instead of the behavior, near-duplicate test methods, or a class grown too large to read. Use it to clean that up without losing a single case of coverage.

  • 8 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 26, 2026
ai-agentsgorefactoring

Security analysis

A100/100

Scanned September 26, 2026

npx -y skills add SebastienDegodez/skraft-plugin --skill test-refactoring-catalog --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Test Refactoring Catalog?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Test Refactoring Catalog
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/sebastiendegodez-test-refactoring-catalog/badge)](https://www.skillsdirectory.com/skills/sebastiendegodez-test-refactoring-catalog)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: test-refactoring-catalog
description: Use when the tests pass but the test code itself has gone bad — the same setup copied into several tests, names that describe the method instead of the behavior, near-duplicate test methods, or a class grown too large to read. Use it to clean that up without losing a single case of coverage.
---

# Test Refactoring Catalog

Safe transformations for test code that preserve behavioral coverage while
improving readability, maintainability, and signal-to-noise ratio.

## When to Load

- After GREEN: tests pass, you notice duplication or unclear naming.
- During COMMIT & VERIFY: cleanup before commit, no new behavior.
- Reviewing test code that smells (long arrange, repeated setup, cryptic names).

## Hard Rule

**Every refactoring below is behavior-preserving.** Run the full test suite
before AND after. If a test turns red, REVERT — the refactoring was wrong.

---

## Catalog

### R1 — Extract Arrange Helper

**Smell:** Multiple tests repeat the same 5+ lines of setup.

**Transform:**
```csharp
// Before — duplicated in 4 tests
var driver = new DriverInfo(Age: 25, LicenseYears: 5);
var vehicle = new VehicleInfo(Type: "sedan", Age: 2);
var policy = new EligibilityPolicy();

// After — one helper, parameters for what varies
private static EligibilityPolicy CreatePolicy() => new();
private static DriverInfo ADriver(int age = 25, int licenseYears = 5) =>
    new(Age: age, LicenseYears: licenseYears);
private static VehicleInfo AVehicle(string type = "sedan", int age = 2) =>
    new(Type: type, Age: age);
```

**Rules:**
- Helper name starts with `A`, `An`, or `Create` + business noun.
- Parameters only for what VARIES across tests. Defaults for the happy path.
- Helper lives in the same test class or a shared `TestKit` project.

---

### R2 — Rename for Business Intent

**Smell:** Test name describes implementation (`WhenHandlerIsCalled_ReturnsTrue`)
instead of business behavior.

**Transform:**
```csharp
// Before
public void Handle_ValidInput_ReturnsSuccess() { ... }

// After
public void WhenDriverMeetsAllCriteria_ShouldBeEligible() { ... }
```

**Pattern:** `When<BusinessCondition>_Should<BusinessOutcome>`

**Rules:**
- No technical words in test names (Handler, Service, Repository, Method).
- Use business vocabulary from the FR→EN lexicon.
- `Should` for expected outcomes, `ShouldNot` / `ShouldReject` for negative cases.

---

### R3 — Consolidate to Parametrized Test

**Smell:** N test methods that differ only in input values, same assertion structure.

**Transform:**
```csharp
// Before — 4 methods with identical structure
[Fact] public void WhenAge17_ShouldBeIneligible() { ... }
[Fact] public void WhenAge16_ShouldBeIneligible() { ... }
[Fact] public void WhenAge15_ShouldBeIneligible() { ... }

// After — one parametrized test
[Theory]
[InlineData(17)]
[InlineData(16)]
[InlineData(15)]
public void WhenDriverIsUnder18_ShouldBeIneligible(int age)
{
    var result = policy.Evaluate(ADriver(age: age), AVehicle());
    result.IsEligible.Should().BeFalse();
}
```

**Rules:**
- Keep the test name general enough to cover all rows.
- Each `[InlineData]` row must represent the same business scenario class.
- If assertions differ between rows → they are NOT the same scenario. Do NOT merge.

---

### R4 — Extract Custom Assertion

**Smell:** Complex multi-line assert repeated across tests.

**Transform:**
```csharp
// Before — repeated in 6 tests
Assert.False(result.IsEligible);
Assert.Equal("driver_too_young", result.Reason.Code);
Assert.Contains("18", result.Reason.Message);

// After
private static void ShouldBeRejectedWith(EligibilityResult result, string reasonCode)
{
    result.IsEligible.Should().BeFalse();
    result.Reason.Code.Should().Be(reasonCode);
}
```

**Rules:**
- Name: `ShouldBe<State>` or `ShouldHave<Property>`.
- Assert helper MUST fail with a clear message on mismatch.
- Never hide assertions inside helpers that also do arrange or act.

---

### R5 — Split Test Class by Scenario Group

**Smell:** One test class with 30+ tests covering multiple independent scenarios.

**Transform:**
```
// Before
EligibilityPolicyTests.cs (35 tests, 6 scenarios mixed)

// After
WhenDriverMeetsAllCriteria_EligibilityTests.cs
WhenDriverIsTooYoung_EligibilityTests.cs
WhenVehicleIsTooOld_EligibilityTests.cs
```

**Rules:**
- Each class = one business scenario group (one `When` clause).
- Shared helpers move to a base class or `TestKit`.
- File name matches class name exactly.

---

### R6 — Inline Trivial Helper

**Smell:** A helper called from exactly ONE test, adding indirection without value.

**Transform:** Inline the helper body back into the test.

**Rule:** If a helper is used once, it's not a helper — it's noise.

---

### R7 — Replace Magic Values with Named Constants

**Smell:** Numeric/string literals in tests with no business meaning visible.

**Transform:**
```csharp
// Before
var driver = new DriverInfo(Age: 25, LicenseYears: 5);

// After
private const int EligibleDriverAge = 25;
private const int MinimumLicenseYears = 5;
var driver = new DriverInfo(Age: EligibleDriverAge, LicenseYears: MinimumLicenseYears);
```

**Rules:**
- Only for values that represent a business threshold or boundary.
- Do NOT name obvious defaults (`0`, `1`, `null`) — they're clear as-is.
- Constant name uses business language, not technical (`MinAge`, not `THRESHOLD_1`).

---

## Decision Flow

```
Test smells detected after GREEN
        │
        ├── Duplicated setup? ──────────→ R1 (Extract Arrange Helper)
        ├── Unclear test name? ─────────→ R2 (Rename for Business Intent)
        ├── N similar test methods? ────→ R3 (Consolidate to Parametrized)
        ├── Repeated assertion block? ──→ R4 (Extract Custom Assertion)
        ├── Bloated test class? ────────→ R5 (Split by Scenario Group)
        ├── Single-use helper? ─────────→ R6 (Inline Trivial Helper)
        └── Magic values? ──────────────→ R7 (Replace with Named Constants)
```

## Anti-Patterns (DO NOT)

| Anti-pattern | Why it's wrong |
|--------------|----------------|
| Refactoring test AND production in same commit | Mixes concerns — can't tell if behavior changed |
| Extracting helper that hides the Act step | Test becomes unreadable — the "what" is gone |
| Renaming tests while red | You don't know if renaming broke something |
| Over-abstracting test setup into inheritance | Deep hierarchies kill test readability |
| Sharing mutable state via class fields | Tests become order-dependent — intermittent failures |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.