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 Type | Speed | Scope | Quantity |
|---|---|---|---|
| Unit | Fast (ms) | Single method | Many (70%) |
| Integration | Medium (seconds) | Multiple components | Some (20%) |
| E2E | Slow (minutes) | Entire system | Few (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:
- RED: Write failing test (defines behavior)
- GREEN: Write simplest code to pass
- 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:
- Test naming convention:
java
// Pattern: methodName_scenario_expectedBehavior
@Test
void withdraw_withInsufficientFunds_throwsException() { }
@Test
void login_withValidCredentials_returnsUser() { }
// Clear what's being tested!- 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");