16.1 TDD Methodology (Red-Green-Refactor) and the Test Pyramid
"It works fine, let's deploy!" is the mindset of an amateur. To escape the constant fear of legacy code exploding unexpectedly whenever you make a modification, you must fundamentally understand TDD (Test-Driven Development), the theoretical foundation for building an application's impenetrable shield.
🏆 1. The Test Pyramid Principle
This is the globally recognized architectural principle referenced most frequently when architecting test code. It defines a hierarchal pyramid dictating exactly which types of tests should be prioritized and heavily emphasized.
- Unit Tests (The Foundation, 80% Proportion)
- This is the fastest, most cost-effective backbone of your testing suite.
- Without booting up Spring or connecting to any Database, it validates the pure Java class module logic (mathematical calculations, etc.) entirely within 0.01 seconds.
- You unequivocally isolate dependencies using Mocking (Fake Objects).
- Integration Tests (The Middle, 15% Proportion)
- This fully boots the Spring Container (
@SpringBootTest) alongside physical Database connections (taking roughly 3~4 seconds) to verify if every distinct layer interacts perfectly together. - Because it is heavy, it is reserved strictly for pipeline verification on critical core sections.
- This fully boots the Spring Container (
- E2E Tests (UI / End-to-End, The Peak, 5% Proportion)
- This mechanically boots a fake browser window (via Selenium, etc.) simulating a real user clicking buttons, executing the entire full-scale scenario directly through to final payment. It is historically the slowest and most expensive.
🚦 2. The 3-Step TDD Breathing Technique: Red - Green - Refactor
TDD is definitively not: "I'll write the code -> Then I'll write the test code later if I have time (actually I'm lazy, skip)." It is an absolute reverse-engineering framework: "You absolutely must first author a guaranteed-to-fail test ➡️ Then you forcefully write the bare minimum operational code actively required just to pass that test ➡️ Then, with total peace of mind, you elegantly tune (Refactor) the internals."
A Practical TDD Scenario: Issuing a Welcome Coupon on Registration
Scenario: "Implement logic ensuring that whenever a new user successfully registers, they unequivocally possess a 10% Welcome Discount Coupon actively inside their wallet."
🍅 Phase 1: RED (Blindly write the Failing Test first)
At this stage, the Member class itself hasn't even been structurally created. However, you simply "pretend it exists" and author the ideal verification statement, completely ignoring the inevitable screaming compiler errors.
class MemberDomainTest {
@Test
@DisplayName("A newly registered user must definitively possess one 10% Welcome Coupon immediately upon registration")
void new_user_has_welcome_coupon() {
// [Given] I don't care, just instantiate a Member class object!
Member newMember = new Member("Alice");
// [When] Execute the registration process
newMember.join();
// [Then] Open the coupon box and precisely verify!
assertThat(newMember.getCoupons()).hasSize(1);
assertThat(newMember.getCoupons().get(0).getRatio()).isEqualTo(0.1); // 10% Discount
}
}
Result: Because the
Memberclass explicitly doesn't exist yet, an explosive red compiler Failure (RED) naturally erupts!
🍏 Phase 2: GREEN (Forge the green light using the most ignorant, fastest method possible)
To silence the screaming red compiler error, you immediately pivot to src/main/java and physically forge the real production code skeleton. To strictly verify if the output design aligns, you simply forcefully hardcode the exact passing value (new Coupon(0.1)).
// Production Code (The Actual Service)
public class Member {
private String name;
private List<Coupon> coupons = new ArrayList<>();
public Member(String name) { this.name = name; }
public void join() {
// Just forcefully unconditionally inject a 10% coupon explicitly purely to pass the test
this.coupons.add(new Coupon(0.1));
}
public List<Coupon> getCoupons() { return this.coupons; }
}
Result: Run the test -> 10% passes beautifully!! Green Light (GREEN)! The defensive shield is formally erected.
♻️ Phase 3: REFACTOR (Polishing with Elegant Architectural Design)
The activation of the Green Light (Shield) signifies a state of absolute invincibility: no matter what you subsequently destroy internally, the monitor will instantly warn you if the logic fails to yield exactly 10%. You cleanly isolate the brutally hardcoded ignorant values into formal Constants, strategically apply a Coupon Generation Policy class, and elegantly transform the internal architecture. Because you instantly know success or failure with a single execution click, your mind remains profoundly at ease.
public void join() {
// Elegant Refactoring: Transitioning from forced hardcoding to an elegant injected Policy Object
Coupon welcomeCoupon = CouponPolicyFactory.createWelcome();
this.coupons.add(welcomeCoupon);
}
// Execute the test again after refactoring -> The GREEN light solidly remains! (Relief)
Relentlessly organically repeating this specific 3-gear rhythmic cycle while continuously expanding the Domain architecture represents the absolute core philosophy of the TDD universe.