How do you practice red-green-refactor TDD in Java with JUnit 5, and how do parameterized tests (@ParameterizedTest) support data-driven test design?
answer
- Red (failing test) → Green (minimum code) → Refactor (safe cleanup)
- Write the test FIRST; minimum code to pass
- @ParameterizedTest + a source annotation
- @ValueSource literals, @CsvSource rows, @MethodSource objects
- Each argument set = its own reported case (boundary/equivalence)
basics
~20 sTDD: write a failing test first (red), write just enough code to pass (green), then clean up the code (refactor), repeating in small loops. JUnit's @ParameterizedTest runs the same test logic over many input sets (via @ValueSource, @CsvSource, @MethodSource) instead of copy-pasting tests.
solid answer
~50 sTest-Driven Development drives design through a tight loop: red — write one small failing test for behavior that doesn't exist yet; green — write the minimum code to make it pass (even something crude); refactor — improve the design while the test stays green, so you're never refactoring without a safety net. The discipline keeps code testable and the suite comprehensive by construction. JUnit 5 supports data-driven testing with @ParameterizedTest plus a source: @ValueSource for simple literals, @CsvSource for inline rows, @EnumSource, and @MethodSource for complex objects produced by a factory method returning a Stream of arguments. This lets one test method cover many equivalence classes and boundary values (a test-design technique) without duplication; each argument set is reported as a separate test case, so a single failing input is pinpointed. I pair TDD's small steps with parameterized tests to cover boundary and edge cases cheaply once the core behavior is green.
code
java · 23 lines// Data-driven: one method, many cases — boundary + equivalence classes
@ParameterizedTest(name = "discount for qty {0} = {1}")
@CsvSource({
"0, 0.0", // boundary: nothing
"1, 0.0", // below threshold
"10, 0.1", // at threshold
"100, 0.2" // bulk
})
void appliesQuantityDiscount(int qty, double expectedRate) {
assertEquals(expectedRate, pricing.discountRate(qty), 1e-9);
}
// Complex objects via a factory
@ParameterizedTest
@MethodSource("users")
void validatesUsers(User u, boolean valid) {
assertEquals(valid, validator.isValid(u));
}
static Stream<Arguments> users() {
return Stream.of(
Arguments.of(new User("[email protected]"), true),
Arguments.of(new User("nope"), false));
}go deeper
Can recite red-green-refactor and write a @ParameterizedTest with @ValueSource or @CsvSource over simple inputs.
Practices the TDD loop in small steps, chooses the right argument source (@CsvSource vs @MethodSource), and applies boundary/equivalence cases as data rows.
Uses TDD to drive design and surface coupling, promotes example tests into parameterized tables, and applies systematic test-design techniques (BVA, decision tables).
Coaches TDD adoption, weighs where it pays off vs not (exploratory code), and shapes data-driven test strategy and naming conventions across teams.
## Test-Driven Development (TDD) **TDD** is a workflow where you write the *test before the production code*. It is language-neutral; here we run it through JUnit 5. The loop has three steps, often called the **red-green-refactor** cycle: 1. **Red** — Write one small test for a behavior that does **not exist yet**. Run it; it fails (or doesn't compile). The failure is the point: it proves the test can fail and defines exactly what "done" means. The bar is red. 2. **Green** — Write the **minimum** production code to make that test pass. It's fine to be crude (even hard-code a return) — the goal is a passing bar quickly, not elegance. The bar turns green. 3. **Refactor** — Now improve the design — rename, extract methods, remove duplication — **while keeping the test green**. Because the test passes throughout, the suite is your safety net: any regression turns the bar red instantly. Then repeat for the next tiny behavior. The benefits: code is **testable by construction** (you can't write untestable code if the test came first), the suite is **comprehensive** (every line exists to pass a test), and design pressure surfaces early (hard-to-test code reveals tight coupling). ### A red-green cycle in JUnit ```java // RED: write the test first; FizzBuzz doesn't exist yet -> fails @Test void threeIsFizz() { assertEquals("Fizz", FizzBuzz.of(3)); } // GREEN: simplest code that passes String of(int n) { return n % 3 == 0 ? "Fizz" : String.valueOf(n); } // REFACTOR: generalize once more tests force it (5 -> Buzz, 15 -> FizzBuzz) ``` A common refinement is the **"transformation priority"** idea: take the smallest step that generalizes the code, driven by adding the next failing test — you don't jump straight to the full algorithm. ## Why parameterized tests fit Many behaviors are really *one rule applied to many inputs*: boundary values, equivalence classes, a truth table. Writing a separate `@Test` per input is repetitive and obscures that they test the *same* logic. **`@ParameterizedTest`** runs one test method repeatedly, once per supplied argument set, each reported as its own case. You pair it with a **source** annotation that provides the data: ```java import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.*; @ParameterizedTest @ValueSource(ints = {2, 4, 100, -8}) // simple literals void evensAreEven(int n) { assertTrue(n % 2 == 0); } @ParameterizedTest @CsvSource({ "3, Fizz", "5, Buzz", "15, FizzBuzz", "7, 7" }) // rows of input,expected void fizzBuzzRules(int input, String expected) { assertEquals(expected, FizzBuzz.of(input)); } @ParameterizedTest @MethodSource("orderCases") // complex objects from a factory void totalsOrders(Order order, Money expected) { assertThat(order.total()).isEqualTo(expected); } static Stream<Arguments> orderCases() { return Stream.of( Arguments.of(new Order(...), Money.of(10)), Arguments.of(new Order(...), Money.of(0))); } ``` Source options: - **`@ValueSource`** — a single array of literals (ints, strings, etc.) bound to one parameter. - **`@CsvSource`** — inline comma-separated rows, each mapped to multiple parameters; great for input→expected tables. - **`@CsvFileSource`** — the same but read from a CSV resource file. - **`@EnumSource`** — one run per enum constant. - **`@MethodSource`** — a static method returning a `Stream<Arguments>` (or collection); the way to pass real objects. - **`@NullSource` / `@EmptySource`** — inject `null`/empty to test edge cases. **Test-design techniques this enables:** *equivalence partitioning* (one representative per class of input), *boundary value analysis* (test just-below/at/just-above limits), and *decision tables* — all expressed as data rows instead of duplicated methods. When one row fails, JUnit names that specific case, so diagnosis is fast. ## How TDD and parameterized tests combine In practice: TDD drives out the *core behavior* one small example at a time (red-green-refactor). Once the logic exists, you **promote** the accumulating examples into a single `@ParameterizedTest` table and add boundary/edge rows — getting broad coverage cheaply without inflating the number of test methods. The parameterized table also becomes living documentation of the rule. ## Mapping to the discipline Red-green-refactor and data-driven testing are universal practices; JUnit 5's `@ParameterizedTest` family is simply Java's mechanism for the data-driven part, and ordinary `@Test` + fast assertions are what make the TDD loop tight enough to be worthwhile.
- When would you use @MethodSource instead of @CsvSource?Use @MethodSource when arguments are non-trivial objects (domain entities, collections, things you can't express as simple CSV literals). It returns a Stream<Arguments> from a static factory, giving full control over object construction; @CsvSource is best for simple scalar input→expected rows.
- What does the 'refactor' step add over just writing tests after the code?It guarantees you always refactor with a passing safety net and that the code was shaped by testability from the start. Test-after risks code that's hard to test and refactors without coverage, so regressions slip through silently.
TDD is like building a staircase by first placing each step's railing (the test) so you can never fall, then laying the step beneath it. Parameterized tests are stamping the same step shape across many positions instead of carving each by hand.
saying these in an interview costs you the question
- Writing production code before the test and calling it TDD (that's test-after)
- Skipping the refactor step, leaving the crude 'green' code in place
- Jumping straight to the full implementation instead of the minimum to pass one test
- Duplicating near-identical @Test methods instead of using @ParameterizedTest
- Putting all cases in one plain @Test so a single failure hides which input broke