Part 4: Testing During Refactoring

Series Navigation: ← Part 3: Refactoring Techniques | Part 5: Automated Tools and CI/CD Integration β†’

Introduction

In Part 3, we covered refactoring techniques. But here's the truth I learned the hard way: refactoring without tests is just changing code and hoping for the best. Early in my career, I "improved" a critical payment service without proper tests. It looked better, passed my manual testing, and broke production in subtle ways that took days to diagnose.

Now, whenever I refactor code in my TypeScript microservices, I follow a strict test-first approach. Tests aren't just nice to haveβ€”they're the safety net that makes confident refactoring possible. In this part, I'll share the testing strategies that have saved me countless times.

The Testing Pyramid

In my microservices, I follow this distribution:

  • 70% Unit Tests: Fast, isolated, test individual functions/classes

  • 20% Integration Tests: Test component interactions

  • 10% E2E Tests: Test complete user flows

This ratio keeps our test suite fast (under 2 minutes for 1,500+ tests) while maintaining confidence in the system.

Step 1: Add Characterization Tests

What Are Characterization Tests?

When refactoring legacy code without tests, start by documenting current behaviorβ€”even if it's wrong. These tests capture "what the code does" not "what it should do."

Real Example: Legacy Order Calculator

I inherited this code in our checkout service:

Before refactoring, write characterization tests:

Benefits:

  • Documents current behavior (good and bad)

  • Provides regression safety during refactoring

  • Identifies bugs to fix after refactoring

  • Builds confidence in the refactoring process

Step 2: Refactor with Tests Passing

Now refactor while keeping tests green:

Update tests to use refactored code:

Unit Testing Strategies

Test Structure: Arrange-Act-Assert

Testing Error Paths

Don't just test the happy path:

Test Helpers and Factories

Create reusable test data builders:

Integration Testing

Testing Repository Layer

Testing Service Integration

Test-Driven Refactoring

Example: Refactoring to Repository Pattern

Current code (direct database access):

Step 1: Write tests for new repository:

Step 2: Implement repository:

Step 3: Update service to use repository:

Step 4: Update service tests:

Testing Refactored Code

Before: Tightly Coupled Code

After: Dependency Injection

Coverage and Quality Metrics

What I Track

In my microservices, I monitor:

Critical paths (payment, auth) require 90%+ coverage

Coverage Reports

Continuous Testing During Development

Watch Mode

This runs tests automatically when files change. I keep this running in a terminal while refactoring.

Pre-commit Hooks

Prevents committing code that breaks tests.

Conclusion

Testing and refactoring go hand-in-hand:

  1. Add characterization tests before refactoring legacy code

  2. Keep tests passing during refactoring

  3. Use dependency injection to make code testable

  4. Test error paths not just happy paths

  5. Use test helpers to reduce duplication

  6. Monitor coverage especially for critical paths

  7. Run tests continuously during development

In Part 5, we'll explore automated tools and CI/CD integration to enforce code quality and testing standards across your team.

Series Navigation: ← Part 3: Refactoring Techniques | Part 5: Automated Tools and CI/CD Integration β†’

Last updated