---
name: refactoring-safely
description: Use when refactoring code - test-preserving transformations in small steps, running tests between each change
---
Refactoring changes code structure without changing behavior; tests must stay green throughout or you're rewriting, not refactoring.
MEDIUM FREEDOM - Follow the change→test→commit cycle strictly, but adapt the specific refactoring patterns to your language and codebase.
| Step | Action | Verify |
|------|--------|--------|
| 1 | Run full test suite | ALL pass |
| 2 | Create bd refactoring task | Track work |
| 3 | Make ONE small change | Compiles |
| 4 | Run tests immediately | ALL still pass |
| 5 | Commit with descriptive message | History clear |
| 6 | Repeat 3-5 until complete | Each step safe |
| 7 | Final verification & close bd | Done |
**Core cycle:** Change → Test → Commit (repeat until complete)
- Improving code structure without changing functionality
- Extracting duplicated code into shared utilities
- Renaming for clarity
- Reorganizing file/module structure
- Simplifying complex code while preserving behavior
**Don't use for:**
- Changing functionality (use feature development)
- Fixing bugs (use hyperpowers:fixing-bugs)
- Adding features while restructuring (do separately)
- Code without tests (write tests first using hyperpowers:test-driven-development)
## 1. Verify Tests Pass
**BEFORE any refactoring:**
```bash
# Use test-runner agent to keep context clean
Dispatch hyperpowers:test-runner agent: "Run: cargo test"
```
**Verify:** ALL tests pass. If any fail, fix them FIRST, then refactor.
**Why:** Failing tests mean you can't detect if refactoring breaks things.
---
## 2. Create bd Task for Refactoring
Track the refactoring work:
```bash
bd create "Refactor: Extract user validation logic" \
--type task \
--priority P2
bd edit bd-456 --design "
## Goal
Extract user validation logic from UserService into separate Validator class.
## Why
- Validation duplicated across 3 services
- Makes testing individual validations difficult
- Violates single responsibility
## Approach
1. Create UserValidator class
2. Extract email validation
3. Extract name validation
4. Extract age validation
5. Update UserService to use validator
6. Remove duplication from other services
## Success Criteria
- All existing tests still pass
- No behavior changes
- Validator has 100% test coverage
"
bd update bd-456 --status in_progress
```
---
## 3. Make ONE Small Change
The smallest transformation that compiles.
**Examples of "small":**
- Extract one method
- Rename one variable
- Move one function to different file
- Inline one constant
- Extract one interface
**NOT small:**
- Extracting multiple methods at once
- Renaming + moving + restructuring
- "While I'm here" improvements
**Example:**
```rust
// Before
fn create_user(name: &str, email: &str) -> Result {
if email.is_empty() {
return Err(Error::InvalidEmail);
}
if !email.contains('@') {
return Err(Error::InvalidEmail);
}
let user = User { name, email };
Ok(user)
}
// After - ONE small change (extract email validation)
fn create_user(name: &str, email: &str) -> Result {
validate_email(email)?;
let user = User { name, email };
Ok(user)
}
fn validate_email(email: &str) -> Result<()> {
if email.is_empty() {
return Err(Error::InvalidEmail);
}
if !email.contains('@') {
return Err(Error::InvalidEmail);
}
Ok(())
}
```
---
## 4. Run Tests Immediately
After EVERY small change:
```bash
Dispatch hyperpowers:test-runner agent: "Run: cargo test"
```
**Verify:** ALL tests still pass.
**If tests fail:**
1. STOP
2. Undo the change: `git restore src/file.rs`
3. Understand why it broke
4. Make smaller change
5. Try again
**Never proceed with failing tests.**
---
## 5. Commit the Small Change
Commit each safe transformation:
```bash
Dispatch hyperpowers:test-runner agent: "Run: git add src/user_service.rs && git commit -m 'refactor(bd-456): extract email validation to function
No behavior change. All tests pass.
Part of bd-456'"
```
**Why commit so often:**
- Easy to undo if next step breaks
- Clear history of transformations
- Can review each step independently
- Proves tests passed at each point
---
## 6. Repeat Until Complete
Repeat steps 3-5 for each small transformation:
```
1. Extract validate_email() ✓ (committed)
2. Extract validate_name() ✓ (committed)
3. Extract validate_age() ✓ (committed)
4. Create UserValidator struct ✓ (committed)
5. Move validations into UserValidator ✓ (committed)
6. Update UserService to use validator ✓ (committed)
7. Remove validation from OrderService ✓ (committed)
8. Remove validation from AccountService ✓ (committed)
```
**Pattern:** change → test → commit (repeat)
---
## 7. Final Verification
After all transformations complete:
```bash
# Full test suite
Dispatch hyperpowers:test-runner agent: "Run: cargo test"
# Linter
Dispatch hyperpowers:test-runner agent: "Run: cargo clippy"
```
**Review the changes:**
```bash
# See all refactoring commits
git log --oneline | grep "bd-456"
# Review full diff
git diff main...HEAD
```
**Checklist:**
- [ ] All tests pass
- [ ] No new warnings
- [ ] No behavior changes
- [ ] Code is cleaner/simpler
- [ ] Each commit is small and safe
**Close bd task:**
```bash
bd edit bd-456 --design "
... (append to existing design)
## Completed
- Created UserValidator class with email, name, age validation
- Removed duplicated validation from 3 services
- All tests pass (verified)
- No behavior changes
- 8 small transformations, each tested
"
bd close bd-456
```
Developer changes behavior while "refactoring"
// Original code
fn validate_email(email: &str) -> Result<()> {
if email.is_empty() {
return Err(Error::InvalidEmail);
}
if !email.contains('@') {
return Err(Error::InvalidEmail);
}
Ok(())
}
// "Refactored" version
fn validate_email(email: &str) -> Result<()> {
if email.is_empty() {
return Err(Error::InvalidEmail);
}
if !email.contains('@') {
return Err(Error::InvalidEmail);
}
// NEW: Added extra validation
if !email.contains('.') { // BEHAVIOR CHANGE
return Err(Error::InvalidEmail);
}
Ok(())
}
- This changes behavior (now rejects emails like "user@localhost")
- Tests might fail, or worse, pass and ship breaking change
- Not refactoring - this is modifying functionality
- Users who relied on old behavior experience regression
**Correct approach:**
1. Extract validation (pure refactoring, no behavior change)
2. Commit with tests passing
3. THEN add new validation as separate feature with new tests
4. Two clear commits: refactoring vs. feature addition
**What you gain:**
- Clear history of what changed when
- Easy to revert feature without losing refactoring
- Tests document exact behavior changes
- No surprises in production
Developer does big-bang refactoring
# Changes made all at once:
- Renamed 15 functions across 5 files
- Extracted 3 new classes
- Moved code between 10 files
- Reorganized module structure
- Updated all import statements
# Then runs tests
$ cargo test
... 23 test failures ...
# Now what? Which change broke what?
- Can't identify which specific change broke tests
- Reverting means losing ALL work
- Fixing requires re-debugging entire refactoring
- Wastes hours trying to untangle failures
- Might give up and revert everything
**Correct approach:**
1. Rename ONE function → test → commit
2. Extract ONE class → test → commit
3. Move ONE file → test → commit
4. Continue one change at a time
**If test fails:**
- Know exactly which change broke it
- Revert ONE commit, not all work
- Fix or make smaller change
- Continue from known-good state
**What you gain:**
- Tests break → immediately know why
- Each commit is reviewable independently
- Can stop halfway with useful progress
- Confidence from continuous green tests
- Clear history for future developers
Developer refactors code without tests
// Legacy code with no tests
fn process_payment(amount: f64, user_id: i64) -> Result {
// 200 lines of complex payment logic
// Multiple edge cases
// No tests exist
}
// Developer refactors without tests:
// - Extracts 5 methods
// - Renames variables
// - Simplifies conditionals
// - "Looks good to me!"
// Deploys to production
// 💥 Payments fail for amounts over $1000
// Edge case handling was accidentally changed
- No tests to verify behavior preserved
- Complex logic has hidden edge cases
- Subtle behavior changes go unnoticed
- Breaks in production, not development
- Costs customer trust and emergency debugging
**Correct approach:**
1. **Write tests FIRST** (using hyperpowers:test-driven-development)
- Test happy path
- Test all edge cases (amounts over $1000, etc.)
- Test error conditions
- Run tests → all pass (documenting current behavior)
2. **Then refactor with tests as safety net**
- Extract method → run tests → commit
- Rename → run tests → commit
- Simplify → run tests → commit
3. **Tests catch any behavior changes immediately**
**What you gain:**
- Confidence behavior is preserved
- Edge cases documented in tests
- Catches subtle changes before production
- Future refactoring is also safe
- Tests serve as documentation
## When to Refactor
- Tests exist and pass
- Changes are incremental
- Business logic stays same
- Can transform in small, safe steps
- Each step independently valuable
## When to Rewrite
- No tests exist (write tests first, then refactor)
- Fundamental architecture change needed
- Easier to rebuild than modify
- Requirements changed significantly
- After 3+ failed refactoring attempts
**Rule:** If you need to change test assertions (not just add tests), you're rewriting, not refactoring.
## Strangler Fig Pattern (Hybrid)
**When to use:**
- Need to replace legacy system but can't tolerate downtime
- Want incremental migration with continuous monitoring
- System too large to refactor in one go
**How it works:**
1. **Transform:** Create modernized components alongside legacy
2. **Coexist:** Both systems run in parallel (façade routes requests)
3. **Eliminate:** Retire old functionality piece by piece
**Example:**
```
Legacy: Monolithic user service (50K LOC)
Goal: Microservices architecture
Step 1 (Transform):
- Create new UserService microservice
- Implement user creation endpoint
- Tests pass in isolation
Step 2 (Coexist):
- Add routing layer (façade)
- Route POST /users to new service
- Route GET /users to legacy service (for now)
- Monitor both, compare results
Step 3 (Eliminate):
- Once confident, migrate GET /users to new service
- Remove user creation from legacy
- Repeat for remaining endpoints
```
**Benefits:**
- Incremental replacement reduces risk
- Legacy continues operating during transition
- Can pause/rollback at any point
- Each migration step is independently valuable
**Use refactoring within components, Strangler Fig for replacing systems.**
## Rules That Have No Exceptions
1. **Tests must stay green** throughout refactoring → If they fail, you changed behavior (stop and undo)
2. **Commit after each small change** → Large commits hide which change broke what
3. **One transformation at a time** → Multiple changes = impossible to debug failures
4. **Run tests after EVERY change** → Delayed testing doesn't tell you which change broke it
5. **If tests fail 3+ times, question approach** → Might need to rewrite instead, or add tests first
## Common Excuses
All of these mean: **Stop and return to the change→test→commit cycle**
- "Small refactoring, don't need tests between steps"
- "I'll test at the end"
- "Tests are slow, I'll run once at the end"
- "Just fixing bugs while refactoring" (bug fixes = behavior changes = not refactoring)
- "Easier to do all at once"
- "I know it works without tests"
- "While I'm here, I'll also..." (scope creep during refactoring)
- "Tests will fail temporarily but I'll fix them" (tests must stay green)
Before marking refactoring complete:
- [ ] All tests pass (verified with hyperpowers:test-runner agent)
- [ ] No new linter warnings
- [ ] No behavior changes introduced
- [ ] Code is cleaner/simpler than before
- [ ] Each commit in history is small and safe
- [ ] bd task documents what was done and why
- [ ] Can explain what each transformation did
**Can't check all boxes?** Return to process and fix before closing bd task.
**This skill requires:**
- hyperpowers:test-driven-development (for writing tests before refactoring if none exist)
- hyperpowers:verification-before-completion (for final verification)
- hyperpowers:test-runner agent (for running tests without context pollution)
**This skill is called by:**
- General development workflows when improving code structure
- After features are complete and working
- When preparing code for new features
**Agents used:**
- test-runner (runs tests/commits without polluting main context)
**Detailed guides:**
- [Common refactoring patterns](resources/refactoring-patterns.md) - Extract Method, Extract Class, Inline, etc.
- [Complete refactoring session example](resources/example-session.md) - Minute-by-minute walkthrough
**When stuck:**
- Tests fail after change → Undo (git restore), make smaller change
- 3+ failures → Question if refactoring is right approach, consider rewrite
- No tests exist → Use hyperpowers:test-driven-development to write tests first
- Unsure how small → If it touches more than one function/file, it's too big