Chapter 1 of 18
01 — Hello, world
Your first JUnit test, assertEquals, and the RED-GREEN-REFACTOR loop.
Kata: a greet(String name) function that returns a greeting.
New ideas: your first JUnit test, @Test, assertEquals, @DisplayName, the TDD loop.
The TDD loop
This chapter exists to build muscle memory for the loop:
- RED — write a test that describes behaviour you want, run it, watch it fail.
- GREEN — write the minimum code to make that test pass.
- REFACTOR — clean up the code, keeping the tests green.
Notice what writing the test first does: it forces you to decide the shape of your API (the method name, its parameter, its return type) before you’ve written any implementation. That’s the whole game.
Step 1 — write a failing test
package hello;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class HelloTest {
@Test
void greetsByName() {
String greeting = Hello.greet("Ada");
assertEquals("Hello, Ada!", greeting);
}
}
A few JUnit facts you’ll use every chapter:
- Test classes go in
src/test/java/<package>/, production code insrc/main/java/<package>/. - Tests are plain methods annotated with
@Test, not functions starting withTest. assertEquals(expected, actual)— note the order: expected first, actual second. The failure diff reads “wanted X, got Y”, so the order keeps the message truthful.- Test classes don’t need to be
public; JUnit 5 runs package-private classes happily.
Run it:
gradle test --tests "hello.HelloTest"
RED. The class Hello doesn’t exist… the test doesn’t even compile. A test that cannot
compile is a failing test — you know the behaviour isn’t there yet.
Step 2 — make it pass
The minimum code. Cheat? No — the minimum:
package hello;
public final class Hello {
private Hello() {
}
public static String greet(String name) {
return "Hello, Ada!";
}
}
Run the tests: GREEN. The test passes… because you hard-coded it.
Step 3 — the real refactor
Now write a second test that forces honest behaviour — greet someone else:
@Test
@DisplayName("greets the whole world when no name is given")
void greetsTheWorldByDefault() {
assertEquals("Hello, World!", Hello.greet(""));
}
Run it: RED (the hard-coded string is wrong for ""). Now generalise:
public static String greet(String name) {
if (name == null || name.isBlank()) {
return "Hello, World!";
}
return "Hello, " + name + "!";
}
The second test drove the real implementation out — the test came first, then the code earned the right to exist.
Java-specific notes
String.isBlank()(Java 11+) — true for""," ", etc.name == nullfirst: anullcheck must come before calling methods on the reference.private Hello() {}— a utility class that should never be instantiated. Java has no package-level free functions, so static methods on a final class are how you expose them.assertEqualswith aString: fails on exact content. In your tests, always decide the exact contract you want to lock in.
Run it
gradle test --tests "hello.HelloTest"
Key takeaway: the test is not a chore — it’s a specification. It defines the method signature and the behaviour. Write it first and the design follows.
Next: 02 — integers, where one failing test teaches assertEquals
and parameterized tests.