Java > Testing in Java > Test-Driven Development (TDD) > Refactoring in TDD

Refactoring in TDD: Extract Method

This snippet demonstrates refactoring in Test-Driven Development (TDD), specifically the 'Extract Method' refactoring technique. After writing a test and implementing the functionality, you might find that a method becomes too long or complex. 'Extract Method' helps by breaking down the larger method into smaller, more manageable, and reusable methods, improving readability and maintainability. The process involves identifying a cohesive block of code within the original method, creating a new method for that block, and then replacing the original block with a call to the new method. Importantly, existing tests should continue to pass after refactoring, validating that the behavior of the code remains unchanged.

Original Code (Before Refactoring)

This code demonstrates a class `Order` that calculates the final price based on discounts applied to the total. The `calculateFinalPrice` method contains multiple `if` statements determining the discount percentage. The `OrderTest` class contains two unit tests that verify the correct calculation of the final price with and without discounts. Notice how the calculation logic is embedded directly within the `calculateFinalPrice` method, making it long and harder to read.

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class Order {
    private double total;
    private double discount;

    public Order(double total) {
        this.total = total;
        this.discount = 0.0;
    }

    public double calculateFinalPrice() {
        double priceAfterDiscount = total;
        if (total > 100) {
            priceAfterDiscount = total * 0.9;
            discount = total * 0.1;
        }
        if (total > 500) {
            priceAfterDiscount = total * 0.8;
            discount = total * 0.2;
        }
        if (total > 1000) {
            priceAfterDiscount = total * 0.7;
            discount = total * 0.3;
        }
        return priceAfterDiscount;
    }

    public double getDiscount() {
        return discount;
    }
}

class OrderTest {
    @Test
    void testCalculateFinalPrice_withDiscount() {
        Order order = new Order(200.0);
        assertEquals(180.0, order.calculateFinalPrice());
        assertEquals(20.0, order.getDiscount());
    }

    @Test
    void testCalculateFinalPrice_noDiscount() {
        Order order = new Order(50.0);
        assertEquals(50.0, order.calculateFinalPrice());
        assertEquals(0.0, order.getDiscount());
    }
}

Refactored Code (After Extract Method)

In this refactored version, the `calculateFinalPrice` method has been simplified. The discount logic is now handled by `applyDiscounts` and a new method called `calculateDiscountedPrice`. `calculateDiscountedPrice` encapsulates the core discount calculation, making the code more readable and easier to maintain. The tests in `OrderTest` remain unchanged and still pass, verifying that the refactoring did not alter the functionality of the code.

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class Order {
    private double total;
    private double discount;

    public Order(double total) {
        this.total = total;
        this.discount = 0.0;
    }

    public double calculateFinalPrice() {
        return applyDiscounts();
    }

    private double applyDiscounts() {
        double priceAfterDiscount = total;
        if (total > 100) {
            priceAfterDiscount = calculateDiscountedPrice(0.9, 0.1);
        }
        if (total > 500) {
            priceAfterDiscount = calculateDiscountedPrice(0.8, 0.2);
        }
        if (total > 1000) {
            priceAfterDiscount = calculateDiscountedPrice(0.7, 0.3);
        }
        return priceAfterDiscount;
    }

    private double calculateDiscountedPrice(double discountRate, double discountAmount) {
        double discountedPrice = total * discountRate;
        discount = total * discountAmount;
        return discountedPrice;
    }

    public double getDiscount() {
        return discount;
    }
}

class OrderTest {
    @Test
    void testCalculateFinalPrice_withDiscount() {
        Order order = new Order(200.0);
        assertEquals(180.0, order.calculateFinalPrice());
        assertEquals(20.0, order.getDiscount());
    }

    @Test
    void testCalculateFinalPrice_noDiscount() {
        Order order = new Order(50.0);
        assertEquals(50.0, order.calculateFinalPrice());
        assertEquals(0.0, order.getDiscount());
    }
}

Concepts Behind the Snippet

The key concepts here are:

  • TDD: Test-Driven Development, where you write tests before writing the code.
  • Refactoring: Improving the internal structure of the code without changing its external behavior.
  • Extract Method: A refactoring technique to break down large methods into smaller, more manageable ones.
  • Code Readability: Making the code easier to understand and maintain.
  • Maintainability: Making the code easier to modify and extend in the future.

Real-Life Use Case

Imagine you are working on a complex e-commerce application where the pricing logic involves many different factors like customer type, product category, promotions, etc. The pricing calculation method might become very long and difficult to understand. By applying the 'Extract Method' refactoring, you can break down the logic into smaller, more focused methods, each responsible for a specific part of the calculation. This will make the code much easier to maintain and debug.

Best Practices

  • Write tests first: Ensure you have adequate test coverage before refactoring.
  • Small steps: Refactor in small, incremental steps to minimize the risk of introducing bugs.
  • Run tests frequently: After each refactoring step, run the tests to ensure that the code still works as expected.
  • Meaningful names: Choose clear and descriptive names for the extracted methods.

When to Use Them

Use 'Extract Method' when:

  • A method is too long and complex.
  • A block of code is repeated in multiple places.
  • A block of code can be logically grouped and given a meaningful name.

Pros

  • Improved code readability and maintainability.
  • Increased code reusability.
  • Reduced code complexity.

Cons

  • Can potentially introduce unnecessary method calls if overused.
  • Requires careful consideration of method names and responsibilities.

Interview Tip

Be prepared to explain the 'Extract Method' refactoring technique and how it contributes to better code quality. Be able to describe a scenario where you have used this technique in a real project and the benefits it brought.

FAQ

  • Why is refactoring important in TDD?

    Refactoring is crucial in TDD because it allows you to improve the design of your code after you have proven that it works correctly through testing. It helps to keep the code clean, maintainable, and adaptable to future changes.
  • How do I know when a method is too long and needs refactoring?

    A good rule of thumb is that a method should ideally fit on a single screen. If a method is longer than that, it might be a good candidate for refactoring. Also, if the method performs multiple distinct tasks, it's likely too complex and should be broken down.