Claude Code Plugins

Community-maintained marketplace

Feedback

test-maintenance-patterns

@tachyon-beep/skillpacks
4
0

Use when reducing test duplication, refactoring flaky tests, implementing page object patterns, managing test helpers, reducing test debt, or scaling test suites - provides refactoring strategies and maintainability patterns for long-term test sustainability

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name test-maintenance-patterns
description Use when reducing test duplication, refactoring flaky tests, implementing page object patterns, managing test helpers, reducing test debt, or scaling test suites - provides refactoring strategies and maintainability patterns for long-term test sustainability

Test Maintenance Patterns

Overview

Core principle: Test code is production code. Apply the same quality standards: DRY, SOLID, refactoring.

Rule: If you can't understand a test in 30 seconds, refactor it. If a test is flaky, fix or delete it.

Test Maintenance vs Writing Tests

Activity When Goal
Writing tests New features, bug fixes Add coverage
Maintaining tests Test suite grows, flakiness increases Reduce duplication, improve clarity, fix flakiness

Test debt indicators:

  • Tests take > 15 minutes to run
  • 5% flakiness rate

  • Duplicate setup code across 10+ tests
  • Tests break on unrelated changes
  • Nobody understands old tests

Page Object Pattern (E2E Tests)

Problem: Duplicated selectors across tests

// ❌ BAD: Selectors duplicated everywhere
test('login', async ({ page }) => {
  await page.fill('#email', 'user@example.com');
  await page.fill('#password', 'password');
  await page.click('button[type="submit"]');
});

test('forgot password', async ({ page }) => {
  await page.fill('#email', 'user@example.com');  // Duplicated!
  await page.click('a.forgot-password');
});

Fix: Page Object Pattern

// pages/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page;
    this.emailInput = page.locator('#email');
    this.passwordInput = page.locator('#password');
    this.submitButton = page.locator('button[type="submit"]');
    this.forgotPasswordLink = page.locator('a.forgot-password');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email, password) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async clickForgotPassword() {
    await this.forgotPasswordLink.click();
  }
}

// tests/login.spec.js
import { LoginPage } from '../pages/LoginPage';

test('login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password');

  await expect(page).toHaveURL('/dashboard');
});

test('forgot password', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.clickForgotPassword();

  await expect(page).toHaveURL('/reset-password');
});

Benefits:

  • Selectors in one place
  • Tests read like documentation
  • Changes to UI require one-line fix

Test Data Builders (Integration/Unit Tests)

Problem: Duplicate test data setup

# ❌ BAD: Duplicated setup
def test_order_total():
    order = Order(
        id=1,
        user_id=123,
        items=[Item(sku="WIDGET", quantity=2, price=10.0)],
        shipping=5.0,
        tax=1.5
    )
    assert order.total() == 26.5

def test_order_discounts():
    order = Order(  # Same setup!
        id=2,
        user_id=123,
        items=[Item(sku="WIDGET", quantity=2, price=10.0)],
        shipping=5.0,
        tax=1.5
    )
    order.apply_discount(10)
    assert order.total() == 24.0

Fix: Builder Pattern

# test_builders.py
class OrderBuilder:
    def __init__(self):
        self._id = 1
        self._user_id = 123
        self._items = []
        self._shipping = 0.0
        self._tax = 0.0

    def with_id(self, id):
        self._id = id
        return self

    def with_items(self, *items):
        self._items = list(items)
        return self

    def with_shipping(self, amount):
        self._shipping = amount
        return self

    def with_tax(self, amount):
        self._tax = amount
        return self

    def build(self):
        return Order(
            id=self._id,
            user_id=self._user_id,
            items=self._items,
            shipping=self._shipping,
            tax=self._tax
        )

# tests/test_orders.py
def test_order_total():
    order = (OrderBuilder()
        .with_items(Item(sku="WIDGET", quantity=2, price=10.0))
        .with_shipping(5.0)
        .with_tax(1.5)
        .build())

    assert order.total() == 26.5

def test_order_discounts():
    order = (OrderBuilder()
        .with_items(Item(sku="WIDGET", quantity=2, price=10.0))
        .with_shipping(5.0)
        .with_tax(1.5)
        .build())

    order.apply_discount(10)
    assert order.total() == 24.0

Benefits:

  • Readable test data creation
  • Easy to customize per test
  • Defaults handle common cases

Shared Fixtures (pytest)

Problem: Setup code duplicated across tests

# ❌ BAD
def test_user_creation():
    db = setup_database()
    user_repo = UserRepository(db)
    user = user_repo.create(email="alice@example.com")
    assert user.id is not None
    cleanup_database(db)

def test_user_deletion():
    db = setup_database()  # Duplicated!
    user_repo = UserRepository(db)
    user = user_repo.create(email="bob@example.com")
    user_repo.delete(user.id)
    assert user_repo.get(user.id) is None
    cleanup_database(db)

Fix: Fixtures

# conftest.py
import pytest

@pytest.fixture
def db():
    """Provide database connection with auto-cleanup."""
    database = setup_database()
    yield database
    cleanup_database(database)

@pytest.fixture
def user_repo(db):
    """Provide user repository."""
    return UserRepository(db)

# tests/test_users.py
def test_user_creation(user_repo):
    user = user_repo.create(email="alice@example.com")
    assert user.id is not None

def test_user_deletion(user_repo):
    user = user_repo.create(email="bob@example.com")
    user_repo.delete(user.id)
    assert user_repo.get(user.id) is None

Reducing Test Duplication

Custom Matchers/Assertions

Problem: Complex assertions repeated

# ❌ BAD: Repeated validation logic
def test_valid_user():
    user = create_user()
    assert user.id is not None
    assert '@' in user.email
    assert len(user.name) > 0
    assert user.created_at is not None

def test_another_valid_user():
    user = create_admin()
    assert user.id is not None  # Same validations!
    assert '@' in user.email
    assert len(user.name) > 0
    assert user.created_at is not None

Fix: Custom assertion helpers

# test_helpers.py
def assert_valid_user(user):
    """Assert user object is valid."""
    assert user.id is not None, "User must have ID"
    assert '@' in user.email, "Email must contain @"
    assert len(user.name) > 0, "Name cannot be empty"
    assert user.created_at is not None, "User must have creation timestamp"

# tests/test_users.py
def test_valid_user():
    user = create_user()
    assert_valid_user(user)

def test_another_valid_user():
    user = create_admin()
    assert_valid_user(user)

Handling Flaky Tests

Strategy 1: Fix the Root Cause

Flaky test symptoms:

  • Passes 95/100 runs
  • Fails with different errors
  • Fails only in CI

Root causes:

  • Race conditions (see flaky-test-prevention skill)
  • Shared state (see test-isolation-fundamentals skill)
  • Timing assumptions

Fix: Use condition-based waiting, isolate state


Strategy 2: Quarantine Pattern

For tests that can't be fixed immediately:

# Mark as flaky, run separately
@pytest.mark.flaky
def test_sometimes_fails():
    # Test code
    pass
# Run stable tests only
pytest -m "not flaky"

# Run flaky tests separately (don't block CI)
pytest -m flaky --count=3  # Retry up to 3 times

Rule: Quarantined tests must have tracking issue. Fix within 30 days or delete.


Strategy 3: Delete If Unfixable

When to delete:

  • Test is flaky AND nobody understands it
  • Test has been disabled for > 90 days
  • Test duplicates coverage from other tests

Better to have: 100 reliable tests than 150 tests with 10 flaky ones


Refactoring Test Suites

Identify Slow Tests

# pytest: Show slowest 10 tests
pytest --durations=10

# Output:
# 10.23s call test_integration_checkout.py::test_full_checkout
# 8.45s call test_api.py::test_payment_flow
# ...

Action: Optimize or split into integration/E2E categories


Parallelize Tests

# pytest: Run tests in parallel
pytest -n 4  # Use 4 CPU cores

# Jest: Run tests in parallel (default)
jest --maxWorkers=4

Requirements:

  • Tests must be isolated (no shared state)
  • See test-isolation-fundamentals skill

Split Test Suites

# pytest.ini
[pytest]
markers =
    unit: Unit tests (fast, isolated)
    integration: Integration tests (medium speed, real DB)
    e2e: End-to-end tests (slow, full system)
# CI: Run test categories separately
jobs:
  unit:
    run: pytest -m unit  # Fast, every commit

  integration:
    run: pytest -m integration  # Medium, every PR

  e2e:
    run: pytest -m e2e  # Slow, before merge

Anti-Patterns Catalog

❌ God Test

Symptom: One test does everything

def test_entire_checkout_flow():
    # 300 lines testing: login, browse, add to cart, checkout, payment, email
    pass

Why bad: Failure doesn't indicate what broke

Fix: Split into focused tests


❌ Testing Implementation Details

Symptom: Tests break when refactoring internal code

# ❌ BAD: Testing internal method
def test_order_calculation():
    order = Order()
    order._calculate_subtotal()  # Private method!
    assert order.subtotal == 100

Fix: Test public interface only

# ✅ GOOD
def test_order_total():
    order = Order(items=[...])
    assert order.total() == 108  # Public method

❌ Commented-Out Tests

Symptom: Tests disabled with comments

# def test_something():
#     # This test is broken, commented out for now
#     pass

Fix: Delete or fix. Create GitHub issue if needs fixing later.


Test Maintenance Checklist

Monthly:

  • Review flaky test rate (should be < 1%)
  • Check build time trend (should not increase > 5%/month)
  • Identify duplicate setup code (refactor into fixtures)
  • Run mutation testing (validate test quality)

Quarterly:

  • Review test coverage (identify gaps)
  • Audit for commented-out tests (delete)
  • Check for unused fixtures (delete)
  • Refactor slowest 10 tests

Annually:

  • Review entire test architecture
  • Update testing strategy for new patterns
  • Train team on new testing practices

Bottom Line

Treat test code as production code. Refactor duplication, fix flakiness, delete dead tests.

Key patterns:

  • Page Objects (E2E tests)
  • Builder Pattern (test data)
  • Shared Fixtures (setup/teardown)
  • Custom Assertions (complex validations)

Maintenance rules:

  • Fix flaky tests immediately or quarantine
  • Refactor duplicated code
  • Delete commented-out tests
  • Split slow test suites

If your tests are flaky, slow, or nobody understands them, invest in maintenance before adding more tests. Test debt compounds like technical debt.