Lesson Completion
Back to course

Testing Best Practices: Unit Testing and TDD

Beginner
12 minutes4.6Java

1. The Hook (The "Byte-Sized" Intro)

  • In a Nutshell: Good tests = Fast, Independent, Repeatable, Self-validating, Timely (FIRST). TDD = Red (write failing test) → Green (make it pass) → Refactor (clean up).
  • Unit tests: Test ONE thing, run in milliseconds.
  • Mocking: Isolate dependencies (Mockito). Coverage ≠ quality (100% coverage doesn't mean good tests!).
  • Test pyramid: Many unit tests, few integration tests, fewer E2E.
  • Golden rule: Test behavior, not implementation. Write tests FIRST!

Think of car manufacturing. Unit tests = test each part (engine, brakes). Integration tests = parts work together. TDD = design-build-verify cycle. Mocking = test steering without whole car. Test pyramid = check bolts everywhere, full crash test once!


2. Conceptual Clarity (The "Simple" Tier)

💡 The Analogy

  • Unit Test: Taste one ingredient (not whole dish)
  • TDD: Test-drive car before buying
  • Mocking: Practice speech with stuffed audience
  • Test Pyramid: Many quick checks, few thorough audits

3. Technical Mastery (The "Deep Dive")

java
// =============================================== // 1. UNIT TESTING BEST PRACTICES // =============================================== // ✅ GOOD: Test one thing, clear name @Test void withdraw_withSufficientFunds_decreasesBalance() { Account account = new Account(100); account.withdraw(30); assertEquals(70, account.getBalance()); } // ❌ BAD: Tests multiple things @Test void testAccount() { Account account = new Account(100); account.withdraw(30); assertEquals(70, account.getBalance()); account.deposit(50); assertEquals(120, account.getBalance()); // ❌ Too many assertions, unclear what failed! } // =========================================== // 2. TDD (Test-Driven Development) // =========================================== // RED: Write failing test first @Test void calculateTotal_withMultipleItems_returnsSumOfPrices() { ShoppingCart cart = new ShoppingCart(); cart.add(new Item("Book", 10.0)); cart.add(new Item("Pen", 2.0)); double total = cart.getTotal(); assertEquals(12.0, total, 0.01); } // ❌ Fails: getTotal() not implemented yet // GREEN: Make it pass (simplest solution) class ShoppingCart { private List<Item> items = new ArrayList<>(); void add(Item item) { items.add(item); } double getTotal() { return items.stream() .mapToDouble(Item::getPrice) .sum(); } } // ✅ Test passes! // REFACTOR: Clean up (test still passes) class ShoppingCart { private final List<Item> items = new ArrayList<>(); void add(Item item) { items.add(item); } double getTotal() { return items.stream() .mapToDouble(Item::getPrice) .sum(); } } // =========================================== // 3. MOCKING (Mockito) // =========================================== // Class under test class OrderService { private final PaymentGateway gateway; private final EmailService emailService; OrderService(PaymentGateway gateway, EmailService emailService) { this.gateway = gateway; this.emailService = emailService; } void processOrder(Order order) { boolean success = gateway.charge(order.getTotal()); if (success) { emailService.sendConfirmation(order.getCustomer()); } } } // ✅ GOOD: Mock dependencies @Test void processOrder_withValidPayment_sendsConfirmation() { // Arrange: Create mocks PaymentGateway mockGateway = mock(PaymentGateway.class); EmailService mockEmail = mock(EmailService.class); OrderService service = new OrderService(mockGateway, mockEmail); Order order = new Order(new Customer("alice@example.com"), 100); // Stub: Define mock behavior when(mockGateway.charge(100)).thenReturn(true); // Act service.processOrder(order); // Assert: Verify email was sent verify(mockEmail).sendConfirmation(any(Customer.class)); } // =========================================== // 4. TEST STRUCTURE (Arrange-Act-Assert) // =========================================== @Test void deposit_withPositiveAmount_increasesBalance() { // Arrange: Set up test data Account account = new Account(100); // Act: Execute behavior account.deposit(50); // Assert: Verify outcome assertEquals(150, account.getBalance()); } // =========================================== // 5. TEST COVERAGE (NOT THE GOAL!) // =========================================== // ❌ BAD: 100% coverage, useless test @Test void testGetName() { User user = new User("Alice"); user.getName(); // ❌ No assertion! Useless! } // 100% coverage doesn't mean good tests! // ✅ GOOD: Test behavior @Test void getName_returnsCorrectName() { User user = new User("Alice"); assertEquals("Alice", user.getName()); } // =========================================== // 6. INDEPENDENT TESTS // =========================================== // ❌ BAD: Tests depend on order static Account sharedAccount; // ❌ Shared state! @Test void testDeposit() { sharedAccount = new Account(100); sharedAccount.deposit(50); assertEquals(150, sharedAccount.getBalance()); } @Test void testWithdraw() { // ❌ Depends on testDeposit running first! sharedAccount.withdraw(30); assertEquals(120, sharedAccount.getBalance()); } // ✅ GOOD: Independent tests @Test void deposit_increasesBalance() { Account account = new Account(100); // Fresh instance account.deposit(50); assertEquals(150, account.getBalance()); } @Test void withdraw_decreasesBalance() { Account account = new Account(100); // Fresh instance account.withdraw(30); assertEquals(70, account.getBalance()); } // =========================================== // 7. TEST PYRAMID // =========================================== // Many unit tests (fast, isolated) @Test void calculateTotal_returnsSumOfPrices() { // Test single method } // Some integration tests @Test @SpringBootTest void placeOrder_savesToDatabaseAndSendsEmail() { // Test multiple components together } // Few end-to-end tests @Test void userCanCompleteCheckoutFlow() { // Test entire user flow (slowest) }

5. The Comparison & Decision Layer

Test TypeSpeedScopeQuantity
UnitFast (ms)Single methodMany (70%)
IntegrationMedium (seconds)Multiple componentsSome (20%)
E2ESlow (minutes)Entire systemFew (10%)

6. The "Interview Corner" (The Edge)

The "Killer" Interview Question: "What is TDD and why use it?" Answer: Test-Driven Development = write test BEFORE code!

Process:

  1. RED: Write failing test (defines behavior)
  2. GREEN: Write simplest code to pass
  3. REFACTOR: Clean up (test ensures behavior preserved)

Benefits:

  • Forces you to think about design first
  • 100% test coverage automatically
  • Tests document expected behavior
  • Refactoring is safe (tests catch breaks)
java
// 1. RED: Write test first @Test void isPalindrome_with_radar_returnsTrue() { assertTrue(isPalindrome("radar")); // ❌ Fails (method doesn't exist) } // 2. GREEN: Implement boolean isPalindrome(String s) { return s.equals(new StringBuilder(s).reverse().toString()); } // ✅ Test passes! // 3. REFACTOR: Optimize boolean isPalindrome(String s) { int left = 0, right = s.length() - 1; while (left < right) { if (s.charAt(left++) != s.charAt(right--)) return false; } return true; } // ✅ Test still passes! Safe refactoring!

Pro-Tips:

  1. Test naming convention:
java
// Pattern: methodName_scenario_expectedBehavior @Test void withdraw_withInsufficientFunds_throwsException() { } @Test void login_withValidCredentials_returnsUser() { } // Clear what's being tested!
  1. Use AssertJ for readable assertions:
java
// ❌ JUnit (harder to read) assertTrue(list.size() > 0); assertEquals("Alice", user.getName()); // ✅ AssertJ (fluent, readable) assertThat(list).isNotEmpty(); assertThat(user.getName()).isEqualTo("Alice");

Topics Covered

Java Fundamentals

Tags

#java#programming#beginner-friendly

Last Updated

2025-02-01