1. java
  2. /testing
  3. /tdd

Master Test-Driven Development (TDD) in Java

Test-Driven Development (TDD) in Java

Test-Driven Development (TDD) is a software development methodology where tests are written before the actual code. This approach ensures that code is thoroughly tested, well-designed, and meets requirements from the start.

Table of Contents

TDD Fundamentals

What is TDD?

Test-Driven Development follows a simple mantra: "Red, Green, Refactor"

  1. Red: Write a failing test
  2. Green: Write minimal code to make the test pass
  3. Refactor: Improve code while keeping tests green

Core Principles

  • Test First: Always write tests before implementation
  • Incremental: Small steps, one test at a time
  • Refactor: Continuously improve code design
  • Fast Feedback: Quick test execution and validation

TDD vs Traditional Development

// Traditional Development
// 1. Write code
// 2. Write tests (maybe)
// 3. Debug issues
// 4. Refactor (sometimes)

// TDD Approach
// 1. Write failing test
// 2. Write minimal code to pass
// 3. Refactor while tests remain green
// 4. Repeat

The TDD Cycle

Step-by-Step Process

// Example: Building a Calculator class using TDD

// STEP 1: RED - Write a failing test
@Test
void shouldAddTwoNumbers() {
    Calculator calculator = new Calculator(); // This doesn't exist yet
    int result = calculator.add(2, 3);
    assertEquals(5, result);
}

// STEP 2: GREEN - Write minimal code to pass
public class Calculator {
    public int add(int a, int b) {
        return 5; // Hardcoded to make test pass
    }
}

// STEP 3: RED - Write another failing test
@Test
void shouldAddDifferentNumbers() {
    Calculator calculator = new Calculator();
    assertEquals(7, calculator.add(3, 4));
    assertEquals(0, calculator.add(-2, 2));
}

// STEP 4: GREEN - Implement proper logic
public class Calculator {
    public int add(int a, int b) {
        return a + b; // Real implementation
    }
}

// STEP 5: REFACTOR - Improve if needed (code is already simple)

Detailed TDD Workflow

class CalculatorTDDTest {
    
    private Calculator calculator;
    
    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }
    
    // Iteration 1: Basic addition
    @Test
    void shouldAddTwoPositiveNumbers() {
        assertEquals(5, calculator.add(2, 3));
    }
    
    // Iteration 2: Handle zero
    @Test
    void shouldAddWithZero() {
        assertEquals(2, calculator.add(2, 0));
        assertEquals(3, calculator.add(0, 3));
    }
    
    // Iteration 3: Handle negative numbers
    @Test
    void shouldAddNegativeNumbers() {
        assertEquals(-1, calculator.add(-2, 1));
        assertEquals(-5, calculator.add(-2, -3));
    }
    
    // Iteration 4: Subtraction
    @Test
    void shouldSubtractNumbers() {
        assertEquals(2, calculator.subtract(5, 3));
        assertEquals(-2, calculator.subtract(3, 5));
    }
    
    // Iteration 5: Division with error handling
    @Test
    void shouldDivideNumbers() {
        assertEquals(2.5, calculator.divide(5, 2), 0.001);
    }
    
    @Test
    void shouldThrowExceptionWhenDividingByZero() {
        assertThrows(IllegalArgumentException.class, 
                    () -> calculator.divide(5, 0));
    }
}

Red-Green-Refactor

Red Phase: Write Failing Test

// Start with a clear requirement and write the test first
class BankAccountTDDTest {
    
    @Test
    void shouldCreateAccountWithInitialBalance() {
        // This test will fail because BankAccount doesn't exist
        BankAccount account = new BankAccount(100.0);
        assertEquals(100.0, account.getBalance(), 0.01);
    }
    
    @Test
    void shouldDepositMoney() {
        BankAccount account = new BankAccount(100.0);
        account.deposit(50.0);
        assertEquals(150.0, account.getBalance(), 0.01);
    }
    
    @Test
    void shouldWithdrawMoney() {
        BankAccount account = new BankAccount(100.0);
        boolean result = account.withdraw(30.0);
        assertTrue(result);
        assertEquals(70.0, account.getBalance(), 0.01);
    }
    
    @Test
    void shouldNotAllowOverdraft() {
        BankAccount account = new BankAccount(50.0);
        boolean result = account.withdraw(100.0);
        assertFalse(result);
        assertEquals(50.0, account.getBalance(), 0.01);
    }
}

Green Phase: Make Tests Pass

// Minimal implementation to make tests pass
public class BankAccount {
    private double balance;
    
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }
    
    public double getBalance() {
        return balance;
    }
    
    public void deposit(double amount) {
        balance += amount;
    }
    
    public boolean withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }
}

Refactor Phase: Improve Design

// Refactored version with better validation and design
public class BankAccount {
    private double balance;
    
    public BankAccount(double initialBalance) {
        validatePositiveAmount(initialBalance, "Initial balance");
        this.balance = initialBalance;
    }
    
    public double getBalance() {
        return balance;
    }
    
    public void deposit(double amount) {
        validatePositiveAmount(amount, "Deposit amount");
        balance += amount;
    }
    
    public boolean withdraw(double amount) {
        validatePositiveAmount(amount, "Withdrawal amount");
        
        if (hasSufficientFunds(amount)) {
            balance -= amount;
            return true;
        }
        return false;
    }
    
    private void validatePositiveAmount(double amount, String operation) {
        if (amount <= 0) {
            throw new IllegalArgumentException(operation + " must be positive");
        }
    }
    
    private boolean hasSufficientFunds(double amount) {
        return amount <= balance;
    }
}

// Add tests for the new validation
@Test
void shouldThrowExceptionForNegativeDeposit() {
    BankAccount account = new BankAccount(100.0);
    assertThrows(IllegalArgumentException.class, 
                () -> account.deposit(-10.0));
}

@Test
void shouldThrowExceptionForNegativeWithdrawal() {
    BankAccount account = new BankAccount(100.0);
    assertThrows(IllegalArgumentException.class, 
                () -> account.withdraw(-10.0));
}

TDD in Practice

Building a Shopping Cart

// Requirements: 
// - Add items to cart
// - Remove items from cart
// - Calculate total
// - Apply discounts

class ShoppingCartTDDTest {
    
    private ShoppingCart cart;
    
    @BeforeEach
    void setUp() {
        cart = new ShoppingCart();
    }
    
    // RED: First failing test
    @Test
    void shouldStartWithEmptyCart() {
        assertTrue(cart.isEmpty());
        assertEquals(0, cart.getItemCount());
    }
    
    // GREEN: Minimal implementation
    // public class ShoppingCart {
    //     public boolean isEmpty() { return true; }
    //     public int getItemCount() { return 0; }
    // }
    
    @Test
    void shouldAddItemToCart() {
        Item item = new Item("Book", 29.99);
        cart.addItem(item);
        
        assertFalse(cart.isEmpty());
        assertEquals(1, cart.getItemCount());
        assertTrue(cart.contains(item));
    }
    
    @Test
    void shouldCalculateTotalForSingleItem() {
        Item item = new Item("Book", 29.99);
        cart.addItem(item);
        
        assertEquals(29.99, cart.getTotal(), 0.01);
    }
    
    @Test
    void shouldCalculateTotalForMultipleItems() {
        cart.addItem(new Item("Book", 29.99));
        cart.addItem(new Item("Pen", 5.99));
        
        assertEquals(35.98, cart.getTotal(), 0.01);
    }
    
    @Test
    void shouldRemoveItemFromCart() {
        Item book = new Item("Book", 29.99);
        Item pen = new Item("Pen", 5.99);
        
        cart.addItem(book);
        cart.addItem(pen);
        
        cart.removeItem(book);
        
        assertEquals(1, cart.getItemCount());
        assertFalse(cart.contains(book));
        assertTrue(cart.contains(pen));
        assertEquals(5.99, cart.getTotal(), 0.01);
    }
    
    @Test
    void shouldHandleQuantities() {
        Item book = new Item("Book", 29.99);
        cart.addItem(book, 3);
        
        assertEquals(1, cart.getItemCount()); // 1 unique item
        assertEquals(3, cart.getQuantity(book));
        assertEquals(89.97, cart.getTotal(), 0.01);
    }
    
    @Test
    void shouldApplyPercentageDiscount() {
        cart.addItem(new Item("Book", 100.0));
        cart.applyDiscount(new PercentageDiscount(10)); // 10% off
        
        assertEquals(90.0, cart.getTotal(), 0.01);
    }
}

// Evolved implementation after multiple TDD cycles
public class ShoppingCart {
    private Map<Item, Integer> items = new HashMap<>();
    private List<Discount> discounts = new ArrayList<>();
    
    public boolean isEmpty() {
        return items.isEmpty();
    }
    
    public int getItemCount() {
        return items.size();
    }
    
    public void addItem(Item item) {
        addItem(item, 1);
    }
    
    public void addItem(Item item, int quantity) {
        items.merge(item, quantity, Integer::sum);
    }
    
    public boolean contains(Item item) {
        return items.containsKey(item);
    }
    
    public void removeItem(Item item) {
        items.remove(item);
    }
    
    public int getQuantity(Item item) {
        return items.getOrDefault(item, 0);
    }
    
    public double getTotal() {
        double subtotal = items.entrySet().stream()
            .mapToDouble(entry -> entry.getKey().getPrice() * entry.getValue())
            .sum();
            
        return applyDiscounts(subtotal);
    }
    
    public void applyDiscount(Discount discount) {
        discounts.add(discount);
    }
    
    private double applyDiscounts(double amount) {
        return discounts.stream()
            .reduce(amount, (acc, discount) -> discount.apply(acc), Double::sum);
    }
}

Building User Authentication Service

class UserAuthenticationServiceTDDTest {
    
    private UserAuthenticationService authService;
    private UserRepository userRepository;
    private PasswordEncoder passwordEncoder;
    
    @BeforeEach
    void setUp() {
        userRepository = mock(UserRepository.class);
        passwordEncoder = mock(PasswordEncoder.class);
        authService = new UserAuthenticationService(userRepository, passwordEncoder);
    }
    
    @Test
    void shouldRegisterNewUser() {
        String username = "john";
        String password = "password123";
        String encodedPassword = "encoded_password";
        
        when(userRepository.existsByUsername(username)).thenReturn(false);
        when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
        
        boolean result = authService.register(username, password);
        
        assertTrue(result);
        verify(userRepository).save(argThat(user -> 
            user.getUsername().equals(username) && 
            user.getPassword().equals(encodedPassword)
        ));
    }
    
    @Test
    void shouldNotRegisterExistingUser() {
        String username = "existing";
        
        when(userRepository.existsByUsername(username)).thenReturn(true);
        
        boolean result = authService.register(username, "password");
        
        assertFalse(result);
        verify(userRepository, never()).save(any());
    }
    
    @Test
    void shouldAuthenticateValidUser() {
        String username = "john";
        String password = "password123";
        String encodedPassword = "encoded_password";
        
        User user = new User(username, encodedPassword);
        when(userRepository.findByUsername(username)).thenReturn(user);
        when(passwordEncoder.matches(password, encodedPassword)).thenReturn(true);
        
        boolean result = authService.authenticate(username, password);
        
        assertTrue(result);
    }
    
    @Test
    void shouldNotAuthenticateInvalidPassword() {
        String username = "john";
        String password = "wrong_password";
        String encodedPassword = "encoded_password";
        
        User user = new User(username, encodedPassword);
        when(userRepository.findByUsername(username)).thenReturn(user);
        when(passwordEncoder.matches(password, encodedPassword)).thenReturn(false);
        
        boolean result = authService.authenticate(username, password);
        
        assertFalse(result);
    }
    
    @Test
    void shouldNotAuthenticateNonExistentUser() {
        when(userRepository.findByUsername("nonexistent")).thenReturn(null);
        
        boolean result = authService.authenticate("nonexistent", "password");
        
        assertFalse(result);
    }
}

// Implementation driven by tests
public class UserAuthenticationService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    
    public UserAuthenticationService(UserRepository userRepository, 
                                   PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }
    
    public boolean register(String username, String password) {
        if (userRepository.existsByUsername(username)) {
            return false;
        }
        
        String encodedPassword = passwordEncoder.encode(password);
        User user = new User(username, encodedPassword);
        userRepository.save(user);
        return true;
    }
    
    public boolean authenticate(String username, String password) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            return false;
        }
        
        return passwordEncoder.matches(password, user.getPassword());
    }
}

Advanced TDD Techniques

Outside-In TDD

// Start with acceptance test (outside)
@Test
void shouldCompleteOrderProcessingWorkflow() {
    // Given
    Customer customer = new Customer("[email protected]");
    Product product = new Product("Book", BigDecimal.valueOf(29.99));
    
    // When
    OrderProcessor processor = new OrderProcessor();
    OrderResult result = processor.processOrder(customer, product, 1);
    
    // Then
    assertThat(result.isSuccessful()).isTrue();
    assertThat(result.getOrder().getStatus()).isEqualTo(OrderStatus.CONFIRMED);
    assertThat(result.getOrder().getTotal()).isEqualTo(BigDecimal.valueOf(29.99));
}

// Then work inward, defining collaborators through tests
class OrderProcessorTest {
    
    @Mock private InventoryService inventoryService;
    @Mock private PaymentService paymentService;
    @Mock private EmailService emailService;
    
    @InjectMocks private OrderProcessor orderProcessor;
    
    @Test
    void shouldProcessOrderWhenInventoryAvailable() {
        // Test drives the design of OrderProcessor and its collaborators
        Customer customer = new Customer("[email protected]");
        Product product = new Product("Book", BigDecimal.valueOf(29.99));
        
        when(inventoryService.isAvailable(product, 1)).thenReturn(true);
        when(paymentService.charge(customer, BigDecimal.valueOf(29.99)))
            .thenReturn(new PaymentResult(true, "txn-123"));
        
        OrderResult result = orderProcessor.processOrder(customer, product, 1);
        
        assertThat(result.isSuccessful()).isTrue();
        verify(emailService).sendOrderConfirmation(any(Order.class));
    }
}

Triangulation in TDD

// Use multiple examples to drive toward general solution
class StringCalculatorTest {
    
    private StringCalculator calculator = new StringCalculator();
    
    @Test
    void shouldReturnZeroForEmptyString() {
        assertEquals(0, calculator.add(""));
    }
    
    @Test
    void shouldReturnNumberForSingleNumber() {
        assertEquals(1, calculator.add("1"));
        assertEquals(5, calculator.add("5"));
    }
    
    @Test
    void shouldAddTwoNumbers() {
        assertEquals(3, calculator.add("1,2"));
        assertEquals(7, calculator.add("3,4"));
        assertEquals(15, calculator.add("7,8"));
    }
    
    @Test
    void shouldHandleMultipleNumbers() {
        assertEquals(6, calculator.add("1,2,3"));
        assertEquals(10, calculator.add("1,2,3,4"));
    }
    
    @Test
    void shouldHandleNewlineDelimiter() {
        assertEquals(6, calculator.add("1\n2,3"));
        assertEquals(10, calculator.add("1\n2\n3,4"));
    }
}

// Implementation evolves through triangulation
public class StringCalculator {
    public int add(String numbers) {
        if (numbers.isEmpty()) {
            return 0;
        }
        
        String[] parts = numbers.split("[,\n]");
        return Arrays.stream(parts)
                    .mapToInt(Integer::parseInt)
                    .sum();
    }
}

TDD with Spring Boot

Controller TDD

@WebMvcTest(UserController.class)
class UserControllerTDDTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void shouldCreateUser() throws Exception {
        CreateUserRequest request = new CreateUserRequest("john", "[email protected]");
        User createdUser = new User(1L, "john", "[email protected]");
        
        when(userService.createUser(any(CreateUserRequest.class)))
            .thenReturn(createdUser);
        
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {
                        "username": "john",
                        "email": "[email protected]"
                    }
                    """))
                .andExpect(status().isCreated())
                .andExpected(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.username").value("john"));
    }
    
    @Test
    void shouldValidateUserInput() throws Exception {
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{}"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errors.username").exists())
                .andExpect(jsonPath("$.errors.email").exists());
    }
}

// Controller implementation driven by tests
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
        User user = userService.createUser(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

Service Layer TDD

@ExtendWith(MockitoExtension.class)
class UserServiceTDDTest {
    
    @Mock private UserRepository userRepository;
    @Mock private EmailService emailService;
    @InjectMocks private UserService userService;
    
    @Test
    void shouldCreateUserAndSendWelcomeEmail() {
        CreateUserRequest request = new CreateUserRequest("john", "[email protected]");
        User savedUser = new User(1L, "john", "[email protected]");
        
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        
        User result = userService.createUser(request);
        
        assertThat(result).isEqualTo(savedUser);
        verify(emailService).sendWelcomeEmail("[email protected]", "john");
    }
    
    @Test
    void shouldThrowExceptionWhenUsernameExists() {
        CreateUserRequest request = new CreateUserRequest("existing", "[email protected]");
        
        when(userRepository.existsByUsername("existing")).thenReturn(true);
        
        assertThrows(UserAlreadyExistsException.class, 
                    () -> userService.createUser(request));
        verify(userRepository, never()).save(any());
    }
}

Benefits and Challenges

Benefits of TDD

// 1. Better Test Coverage - Every line of code has a test
class BenefitsExampleTest {
    
    @Test
    void demonstratesBetterDesign() {
        // TDD forces you to think about API design first
        PaymentProcessor processor = new PaymentProcessor();
        
        // Clear, testable interface
        PaymentResult result = processor.processPayment(
            new PaymentRequest("4111111111111111", BigDecimal.valueOf(100))
        );
        
        assertThat(result.isSuccessful()).isTrue();
    }
    
    @Test
    void demonstratesDocumentation() {
        // Tests serve as living documentation
        Calculator calc = new Calculator();
        
        // Business rule: division by zero should throw exception
        assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
        
        // Business rule: negative numbers allowed
        assertEquals(-5, calc.add(-2, -3));
    }
}

// 2. Faster Feedback Loop
// - Tests fail immediately when you break something
// - No need to manually test through UI
// - Regression detection is automatic

// 3. Refactoring Confidence
public class RefactoringExample {
    // Original implementation
    public double calculateTax_v1(double amount, String state) {
        if (state.equals("CA")) {
            return amount * 0.08;
        } else if (state.equals("NY")) {
            return amount * 0.07;
        }
        return amount * 0.05;
    }
    
    // Refactored with tests providing safety net
    private static final Map<String, Double> TAX_RATES = Map.of(
        "CA", 0.08,
        "NY", 0.07
    );
    
    public double calculateTax_v2(double amount, String state) {
        double rate = TAX_RATES.getOrDefault(state, 0.05);
        return amount * rate;
    }
}

Common Challenges

class TDDChallengesTest {
    
    // Challenge 1: Learning curve - need to think differently
    @Test
    void challengeThinkingTestFirst() {
        // Old habit: implement then test
        // New habit: test then implement
        
        // It takes practice to write good tests first
        Calculator calc = new Calculator();
        assertThat(calc.multiply(3, 4)).isEqualTo(12);
    }
    
    // Challenge 2: Slower initial development
    @Test
    void challengeInitialSpeed() {
        // TDD might seem slower at first
        // But prevents bugs and rework later
        // Overall development speed increases
    }
    
    // Challenge 3: Testing external dependencies
    @Test
    void challengeExternalDependencies() {
        // Mock external systems in unit tests
        ExternalService mockService = mock(ExternalService.class);
        when(mockService.getData()).thenReturn("test data");
        
        MyService service = new MyService(mockService);
        String result = service.processData();
        
        assertThat(result).contains("test data");
    }
}

Best Practices

Effective TDD Workflow

class TDDBestPracticesTest {
    
    // 1. Start with simplest test
    @Test
    void startSimple() {
        FizzBuzz fizzBuzz = new FizzBuzz();
        assertEquals("1", fizzBuzz.convert(1));
    }
    
    // 2. Write failing test first
    @Test
    void alwaysStartWithFailingTest() {
        // This test should fail when first written
        FizzBuzz fizzBuzz = new FizzBuzz();
        assertEquals("Fizz", fizzBuzz.convert(3));
    }
    
    // 3. Write minimal code to pass
    public class FizzBuzz {
        public String convert(int number) {
            if (number % 3 == 0) return "Fizz";
            return String.valueOf(number);
        }
    }
    
    // 4. Add more specific tests
    @Test
    void shouldReturnBuzzForMultiplesOfFive() {
        FizzBuzz fizzBuzz = new FizzBuzz();
        assertEquals("Buzz", fizzBuzz.convert(5));
        assertEquals("Buzz", fizzBuzz.convert(10));
    }
    
    @Test
    void shouldReturnFizzBuzzForMultiplesOfBoth() {
        FizzBuzz fizzBuzz = new FizzBuzz();
        assertEquals("FizzBuzz", fizzBuzz.convert(15));
        assertEquals("FizzBuzz", fizzBuzz.convert(30));
    }
    
    // 5. Refactor when tests are green
    public class FizzBuzzRefactored {
        public String convert(int number) {
            StringBuilder result = new StringBuilder();
            
            if (number % 3 == 0) result.append("Fizz");
            if (number % 5 == 0) result.append("Buzz");
            
            return result.length() > 0 ? result.toString() : String.valueOf(number);
        }
    }
}

class TDDGuidelines {
    
    // DO: Keep tests simple and focused
    @Test
    void goodTestExample() {
        Calculator calc = new Calculator();
        assertEquals(5, calc.add(2, 3));
    }
    
    // DON'T: Test multiple things in one test
    @Test
    void badTestExample() {
        Calculator calc = new Calculator();
        assertEquals(5, calc.add(2, 3));          // Addition
        assertEquals(1, calc.subtract(3, 2));     // Subtraction  
        assertEquals(6, calc.multiply(2, 3));     // Multiplication
        // Too many responsibilities in one test
    }
    
    // DO: Use descriptive test names
    @Test
    void shouldThrowExceptionWhenDividingByZero() {
        Calculator calc = new Calculator();
        assertThrows(ArithmeticException.class, () -> calc.divide(5, 0));
    }
    
    // DON'T: Use vague test names
    @Test
    void testDivision() {
        // What aspect of division is being tested?
    }
    
    // DO: Follow the AAA pattern
    @Test
    void shouldCalculateOrderTotal() {
        // Arrange
        Order order = new Order();
        order.addItem(new Item("Book", BigDecimal.valueOf(29.99)));
        order.addItem(new Item("Pen", BigDecimal.valueOf(5.99)));
        
        // Act
        BigDecimal total = order.calculateTotal();
        
        // Assert
        assertEquals(BigDecimal.valueOf(35.98), total);
    }
}

TDD Anti-Patterns to Avoid

class TDDAntiPatternsTest {
    
    // ANTI-PATTERN: Writing tests after implementation
    // DON'T: Write code first, then add tests
    
    // ANTI-PATTERN: Testing implementation details
    @Test
    void badTestingImplementationDetails() {
        UserService service = new UserService();
        // Testing internal method calls instead of behavior
        // verify(service.getValidator()).validate(any()); // Don't do this
    }
    
    // BETTER: Test behavior and outcomes
    @Test
    void goodTestingBehavior() {
        UserService service = new UserService();
        User user = service.createUser("john", "[email protected]");
        assertThat(user.getUsername()).isEqualTo("john");
    }
    
    // ANTI-PATTERN: Overly complex tests
    @Test
    void overlyComplexTest() {
        // Avoid complex setup and multiple assertions
        // If test is complex, break it down
    }
    
    // ANTI-PATTERN: Ignoring refactoring phase
    // DON'T: Skip refactoring after making tests pass
    // DO: Always improve code structure when tests are green
}

Summary

Test-Driven Development is a powerful methodology that improves code quality and design:

Core Process:

  • Red: Write a failing test first
  • Green: Write minimal code to make it pass
  • Refactor: Improve design while keeping tests green

Key Benefits:

  • Better Design: Forces thinking about API and interfaces first
  • High Test Coverage: Every line of code is tested
  • Living Documentation: Tests explain how code should behave
  • Refactoring Confidence: Safe to change code with comprehensive tests
  • Faster Feedback: Immediate notification of regressions

TDD Techniques:

  • Baby Steps: Small increments, one test at a time
  • Triangulation: Use multiple examples to drive general solutions
  • Outside-In: Start with acceptance tests, work inward
  • Mock Dependencies: Isolate units under test

Best Practices:

  • Write simplest failing test first
  • Make it pass with minimal code
  • Refactor fearlessly when tests are green
  • Keep tests simple and focused
  • Use descriptive test names
  • Follow Arrange-Act-Assert pattern

Common Pitfalls:

  • Writing tests after implementation
  • Testing implementation details instead of behavior
  • Overly complex test setup
  • Skipping the refactoring phase

TDD requires discipline and practice but leads to more robust, maintainable, and well-designed Java applications.