|
27 | 27 | <p> |
28 | 28 | Picture this: You join a new team, and your first task is to fix a bug in a legacy billing module. The method is 300 lines long, filled with cryptic variable names and nested ifs. You hesitate—what if you break something? This fear is universal. But with a solid suite of tests, refactoring transforms from a risky gamble into a safe, even enjoyable, process. Tests are your safety net, letting you clean up code with confidence. |
29 | 29 | </p> |
30 | | - <b>Imagine a scenario:</b> You inherit an 8-year-old payment processing function mixing currency conversion, platform fees, and tax logic. Write 18 characterization tests covering edge cases. Now refactor safely: split into `CalculatePlatformFee()`, `ApplyTaxes()`, `ConvertCurrency()`. Tests verify all three work together. |
31 | 30 | </Section> |
32 | 31 | <Section Heading="How to Decide What to Refactor First" Level="4"> |
33 | 32 | <p> |
34 | | - Not sure where to start? Focus your refactoring efforts where they’ll have the biggest impact: |
| 33 | + In TDD, refactoring opportunities emerge naturally from your test suite. Focus your refactoring efforts where they'll have the biggest impact: |
35 | 34 | </p> |
36 | 35 | <ul> |
37 | | - <li><b>High-churn areas:</b> Code that changes often is more likely to accumulate bugs and technical debt. Clean it up first.</li> |
38 | | - <li><b>Pain points:</b> Functions or modules that frustrate you or your team—slow to understand, hard to modify, or error-prone.</li> |
39 | | - <li><b>Repetitive code:</b> Copy-pasted logic or duplicated patterns are prime candidates for extraction and simplification.</li> |
40 | | - <li><b>Frequent bugs:</b> If a part of the codebase is always breaking, refactor it to make future fixes easier.</li> |
| 36 | + <li><b>High-churn areas with weak tests:</b> Code that changes often but lacks comprehensive test coverage is a refactoring candidate. Write tests first (characterization tests if it's legacy code), then refactor safely.</li> |
| 37 | + <li><b>Failing or flaky tests:</b> If tests are hard to write or maintain for a feature, the underlying code likely needs refactoring. Use the test feedback as a signal that design is off.</li> |
| 38 | + <li><b>Repetitive test patterns:</b> Duplicated test logic or setup code suggests the production code should be simplified. Refactor to reduce test boilerplate.</li> |
| 39 | + <li><b>Complex test assertions:</b> If verifying behavior requires many assertions or helper methods, the code under test is probably doing too much. Refactor to single responsibility.</li> |
41 | 40 | </ul> |
42 | 41 | <CalloutBox Type="info"> |
43 | | - <b>Tip for juniors:</b> Start small. Refactor a single function or file, and build confidence as your tests protect you. |
| 42 | + <b>Tip for juniors:</b> Let your tests guide you. If a test is hard to write, refactor the code to make it testable. Testability and clean design go hand-in-hand. |
44 | 43 | </CalloutBox> |
45 | 44 | </Section> |
46 | 45 |
|
|
49 | 48 | Refactoring isn’t rewriting, slowing down, or sneaking in new features. It’s about <b>improving the internal structure without changing what the code does</b>. The challenge? Sometimes the code is so tangled, or so poorly tested, that even small changes feel risky. If you’ve ever tried to clean up a function and ended up breaking something, you know the pain. |
50 | 49 | </p> |
51 | 50 | <b>Example:</b> Discount logic is duplicated in `OrderService`, `CartService`, and `QuoteService`. Write 24 tests covering percentage, flat-rate, seasonal, and tier-based discounts. Extract a single `DiscountCalculator` class. Now one bug fix to senior-citizen discounts benefits all three services. |
| 51 | + <p> |
| 52 | + <b>Step-by-step walkthrough:</b> |
| 53 | + </p> |
| 54 | + <ol> |
| 55 | + <li> |
| 56 | + <b>Before refactoring:</b> Each service calculates discounts independently, leading to bugs when business rules change. |
| 57 | + <CodeSnippet Language="csharp" Description="Duplicated Discount Logic"> |
| 58 | + public class OrderService |
| 59 | + { |
| 60 | + public decimal CalculateTotal(Order order) |
| 61 | + { |
| 62 | + decimal discount = 0; |
| 63 | + if (order.CustomerType == "Senior") discount = order.Total * 0.15m; |
| 64 | + else if (order.CustomerType == "Premium") discount = order.Total * 0.10m; |
| 65 | + return order.Total - discount; |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + public class CartService |
| 70 | + { |
| 71 | + public decimal CalculateTotal(Cart cart) |
| 72 | + { |
| 73 | + decimal discount = 0; |
| 74 | + if (cart.CustomerType == "Senior") discount = cart.Total * 0.15m; |
| 75 | + else if (cart.CustomerType == "Premium") discount = cart.Total * 0.10m; |
| 76 | + return cart.Total - discount; |
| 77 | + } |
| 78 | + } |
| 79 | + </CodeSnippet> |
| 80 | + </li> |
| 81 | + <li> |
| 82 | + <b>Write tests first:</b> Cover all discount scenarios to lock in expected behavior. |
| 83 | + <CodeSnippet Language="csharp" Description="Comprehensive Discount Tests"> |
| 84 | + [Fact] |
| 85 | + public void CalculateDiscount_SeniorCustomer_ShouldReturn15Percent() => |
| 86 | + Assert.Equal(150m, calculator.CalculateDiscount(100m, "Senior")); |
| 87 | + |
| 88 | + [Fact] |
| 89 | + public void CalculateDiscount_PremiumCustomer_ShouldReturn10Percent() => |
| 90 | + Assert.Equal(90m, calculator.CalculateDiscount(100m, "Premium")); |
| 91 | + |
| 92 | + [Fact] |
| 93 | + public void CalculateDiscount_RegularCustomer_ShouldReturnNoDiscount() => |
| 94 | + Assert.Equal(100m, calculator.CalculateDiscount(100m, "Regular")); |
| 95 | + </CodeSnippet> |
| 96 | + </li> |
| 97 | + <li> |
| 98 | + <b>After refactoring:</b> Extract discount logic into a single, reusable class. |
| 99 | + <CodeSnippet Language="csharp" Description="Centralized DiscountCalculator"> |
| 100 | + public class DiscountCalculator |
| 101 | + { |
| 102 | + public decimal CalculateDiscount(decimal total, string customerType) => customerType switch |
| 103 | + { |
| 104 | + "Senior" => total * 0.15m, |
| 105 | + "Premium" => total * 0.10m, |
| 106 | + _ => 0m |
| 107 | + }; |
| 108 | + } |
| 109 | + |
| 110 | + public class OrderService |
| 111 | + { |
| 112 | + private readonly DiscountCalculator discountCalc = new(); |
| 113 | + public decimal CalculateTotal(Order order) => |
| 114 | + order.Total - discountCalc.CalculateDiscount(order.Total, order.CustomerType); |
| 115 | + } |
| 116 | + |
| 117 | + public class CartService |
| 118 | + { |
| 119 | + private readonly DiscountCalculator discountCalc = new(); |
| 120 | + public decimal CalculateTotal(Cart cart) => |
| 121 | + cart.Total - discountCalc.CalculateDiscount(cart.Total, cart.CustomerType); |
| 122 | + } |
| 123 | + </CodeSnippet> |
| 124 | + </li> |
| 125 | + <li> |
| 126 | + <b>Benefit:</b> Fix a bug once, fix it everywhere. Change senior-citizen discount from 15% to 20%? Update one line in `DiscountCalculator`. All three services benefit immediately. Tests ensure nothing breaks. |
| 127 | + </li> |
| 128 | + </ol> |
52 | 129 | </Section> |
53 | 130 |
|
54 | 131 | <Section Heading="The Golden Rule of Refactoring" Level="4"> |
|
94 | 171 | <li><b>Refactor:</b> Now, clean up the code. The tests guarantee you don’t break anything.</li> |
95 | 172 | </ol> |
96 | 173 | <p> |
97 | | - <b>Example:</b> Express shipments should cost $15 flat, but are being charged weight-based rates. Red: `Assert.Equal(15, calculator.CalculateExpressShippingFee(anyWeight))` fails. Green: simplest fix. Refactor: introduce `ShippingStrategy` interface with implementations. Tests lock down both express and weight-tier rates working correctly. |
| 174 | + <b>Example:</b> Your shipping calculator has a bug: express shipments should cost $15 flat, but the system is incorrectly charging weight-based rates instead. |
98 | 175 | </p> |
| 176 | + <ol> |
| 177 | + <li><b>Red:</b> Write a test that locks in the correct behavior: |
| 178 | + <CodeSnippet Language="csharp" Description="Test for Express Shipping"> |
| 179 | + [Fact] |
| 180 | + public void CalculateExpressShippingFee_ShouldReturn15Flat() |
| 181 | + { |
| 182 | + var calculator = new ShippingCalculator(); |
| 183 | + var result = calculator.CalculateExpressShippingFee(weight: 50); // weight shouldn't matter |
| 184 | + Assert.Equal(15m, result); |
| 185 | + } |
| 186 | + </CodeSnippet> |
| 187 | + The test fails because the code applies weight-based logic to express shipments. |
| 188 | + </li> |
| 189 | + <li><b>Green:</b> Make the test pass with the simplest fix—hardcode $15 for express shipments or add a conditional check. |
| 190 | + </li> |
| 191 | + <li><b>Refactor:</b> Now that tests protect you, introduce a `ShippingStrategy` interface to handle different shipping types cleanly: |
| 192 | + <CodeSnippet Language="csharp" Description="Refactored Shipping Strategy"> |
| 193 | + public interface IShippingStrategy |
| 194 | + { |
| 195 | + decimal CalculateFee(decimal weight); |
| 196 | + } |
| 197 | + |
| 198 | + public class ExpressShippingStrategy : IShippingStrategy |
| 199 | + { |
| 200 | + public decimal CalculateFee(decimal weight) => 15m; // flat rate |
| 201 | + } |
| 202 | + |
| 203 | + public class StandardShippingStrategy : IShippingStrategy |
| 204 | + { |
| 205 | + public decimal CalculateFee(decimal weight) => weight * 0.5m; |
| 206 | + } |
| 207 | + </CodeSnippet> |
| 208 | + All tests still pass. Both express and weight-tier shipping now work correctly, and adding new shipping types is easy. |
| 209 | + </li> |
| 210 | + </ol> |
99 | 211 | </Section> |
100 | 212 |
|
101 | 213 | <Section Heading="Practical Example — The Ugly Function Makeover" Level="4"> |
|
195 | 307 | </Section> |
196 | 308 |
|
197 | 309 | <Section Heading="Refactoring Patterns You Should Know" Level="4"> |
| 310 | + <p> |
| 311 | + In TDD, refactoring patterns emerge from your Red-Green-Refactor cycle. Here are essential patterns and how they fit into TDD: |
| 312 | + </p> |
198 | 313 | <ul> |
199 | | - <li>Extract Method</li> |
200 | | - <li>Extract Class</li> |
201 | | - <li>Inline Variable</li> |
202 | | - <li>Rename for Intent</li> |
203 | | - <li>Introduce Parameter Object</li> |
204 | | - <li>Replace Magic Numbers with Constants</li> |
205 | | - <li>Move Method</li> |
206 | | - <li>Encapsulate Collection</li> |
207 | | - <li>Replace Conditional with Polymorphism</li> |
| 314 | + <li><b>Extract Method:</b> Test reveals a method does too much? Extract a helper. Write a test for the extracted behavior first.</li> |
| 315 | + <li><b>Extract Class:</b> When a class has too many responsibilities (and your tests for it are complex), split it. Tests guide which responsibilities belong together.</li> |
| 316 | + <li><b>Inline Variable:</b> Overly complex intermediate variables make tests harder to read. Inline them once tests verify the logic.</li> |
| 317 | + <li><b>Rename for Intent:</b> If a test name doesn't match what the code does, rename the code. Tests act as specifications.</li> |
| 318 | + <li><b>Introduce Parameter Object:</b> When you have many parameters making tests verbose, group them. Tests tell you when parameters need organization.</li> |
| 319 | + <li><b>Replace Magic Numbers with Constants:</b> Tests should use named constants, not magic numbers. This refactor improves both code and test readability.</li> |
| 320 | + <li><b>Replace Conditional with Polymorphism:</b> Complex conditionals (if/else chains) are test nightmares. Strategy or State patterns simplify testing.</li> |
208 | 321 | </ul> |
209 | | - <p>Learn a few — not all. Use them often. Try applying one pattern in your next refactor and see the difference.</p> |
| 322 | + <p> |
| 323 | + <b>Example - Extract Method via TDD:</b> Your test is failing because a method mixes currency conversion with discount calculation. |
| 324 | + </p> |
| 325 | + <CodeSnippet Language="csharp" Description="Test Reveals Need for Extraction"> |
| 326 | + [Fact] |
| 327 | + public void CalculatePrice_WithCurrencyAndDiscount_ShouldWorkCorrectly() |
| 328 | + { |
| 329 | + // This test is trying to verify too much at once |
| 330 | + var result = calculator.CalculatePrice(100m, "USD", "Senior"); |
| 331 | + Assert.Equal(expectedValue, result); |
| 332 | + } |
| 333 | + |
| 334 | + // Refactor: Write separate tests |
| 335 | + [Fact] |
| 336 | + public void ConvertCurrency_USD_ShouldApplyCorrectRate() => ... |
| 337 | + |
| 338 | + [Fact] |
| 339 | + public void ApplyDiscount_SeniorCustomer_ShouldReturn15Percent() => ... |
| 340 | + |
| 341 | + // Now extract the methods to make tests pass |
| 342 | + private decimal ConvertCurrency(decimal amount, string currency) => ... |
| 343 | + private decimal ApplyDiscount(decimal amount, string customerType) => ... |
| 344 | + </CodeSnippet> |
| 345 | + <p> |
| 346 | + Learn a few patterns and use them often. Let your tests guide which pattern fits best. |
| 347 | + </p> |
210 | 348 | </Section> |
211 | 349 |
|
212 | 350 | <Section Heading="Common Gotchas & Anti-Patterns" Level="4"> |
|
234 | 372 | Mutation testing helps verify your tests are strong enough to detect broken behavior. It works by making small changes (mutations) to your code and checking if your tests fail as expected. |
235 | 373 | </p> |
236 | 374 | <CodeSnippet Language="csharp" Description="Mutation Testing Example"> |
237 | | - // Original code |
238 | | - return cart.Price * 0.9m; |
239 | | - // Mutated code (by tool) |
240 | | - return cart.Price * 0.8m; |
241 | | - // If your test still passes, it’s too weak! |
| 375 | + // Original code |
| 376 | + return cart.Price * 0.9m; |
| 377 | + // Mutated code (by tool) |
| 378 | + return cart.Price * 0.8m; |
| 379 | + // If your test still passes, it’s too weak! |
242 | 380 | </CodeSnippet> |
243 | 381 | <p>Tools like Stryker.NET or Pitest (Java) automate mutation testing and help you strengthen your test suite.</p> |
244 | 382 | </Section> |
|
0 commit comments