Learn Java with Tests

A TDD course for Java, in the spirit of Learn Go with Tests.

Chapter 3 of 18

03 — Iteration

Loops, StringBuilder, and refactoring to String.repeat.

Kata: Repeater.repeat(text, times) returns text repeated times times. New ideas: a for loop; test-driven discovery that one test isn’t enough; refactoring away your own code in favour of the standard library.

Step 1 — write just enough of a test

Resist the big design up front. Write the smallest test that fails:

package iteration;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class RepeaterTest {

    @Test
    void repeatsACharacterFiveTimes() {
        assertEquals("aaaaa", Repeater.repeat("a", 5));
    }
}

Run it: RED, Repeater doesn’t exist.

Step 2 — minimum code

Cheat, but deliberately:

public static String repeat(String text, int times) {
    return text + text + text + text + text;
}

GREEN — and completely fake. This is normal. Now let the loop be driven by more tests.

Step 3 — the test that forces a loop

You know times is a parameter. Write a failing case for a different repetition count:

@Test
void repeatsMoreThanOneCharacter() {
    assertEquals("ababab", Repeater.repeat("ab", 3));
}

RED. Now you can’t hard-code five texts. Time for the loop — and while you’re here, notice why the loop uses a StringBuilder rather than +:

public static String repeat(String text, int times) {
    StringBuilder out = new StringBuilder();
    for (int i = 0; i < times; i++) {
        out.append(text);
    }
    return out.toString();
}

REFACTOR — know the standard library

Java 11 ships String.repeat. Your loop is that method. Learning to spot “my code already exists in the JDK” is a real skill:

public static String repeat(String text, int times) {
    return text.repeat(times);
}

GREEN, tested, shorter. The refactor step was not optional — it’s where you learn the language. The tests protected you while you replaced the implementation.

Java-specific notes

  • StringBuilder.appendString is immutable in Java; text + text + ... in a loop creates a new string every iteration (O(n²)). StringBuilder is the escape hatch.
  • for (int i = 0; i < times; i++) is the classic loop. Everything else — while, for-each, IntStream.range — has its place; TDD will reveal why each exists in later chapters.
  • Edge cases are cheap once the loop exists: a repeatsZeroTimes test for times = 0 is already covered by the same loop. Add a test anyway — it documents the contract.

Run it

gradle test --tests "iteration.RepeaterTest"

Key takeaway: the second test forced the honest implementation. Don’t design in your head; write a slightly different requirement and let your code evolve under the tests.

Next: 04 — collections — lists and sums.