
Test-driven development is not about testing. Test-driven development is about development (and design), specifically improving the quality and design of code. The resulting unit tests are just an extremely useful by-product.
TDD life-cycle
Before explaining best practices, it is important to understand the TDD life-cycle.
- Write the test
- Run the test (there is no implementation code, test does not pass)
- Write just enough implementation code to make the test pass
- Run all tests (tests pass)
- Refactor
- Repeat

Result
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit5Parent; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class HelloWorld6FinalTest extends JUnit5Parent { @Test void print_Hello_World_by_default() { String expected = "Hello, World!!"; HelloWorld6Final.main(null); Assertions.assertEquals(expected, getOutput()); } @DisplayName("print Hello with parameter (expected, parameter)") @ParameterizedTest @CsvSource({ "'Hello, Damien!', Damien", "'Hello, Jane!', Jane", "'Hello, John!', John", }) void print_Hello_with_parameter(String expected, String parameter) { String[] args = {parameter}; HelloWorld6Final.main(args); Assertions.assertEquals(expected, getOutput()); } @DisplayName("exception bad format (description, expected, parameter)") @ParameterizedTest @CsvSource({ "Param is empty, bad arg (ex: Damien), '' ", "Param is blanck, bad arg (ex: Damien), ' ' ", "Param is null, bad arg (ex: Damien), null" }) void exception_bad_format(String description, String expected, String parameter) { String[] args = {parameter.equals("null") ? null : parameter}; Exception e = Assertions.assertThrows(IllegalArgumentException.class, () -> { HelloWorld6Final.main(args); }); Assertions.assertEquals(expected, e.getMessage()); } }
Specifications
This tutorial will implement the following specifications.
- Print “Hello, World!”
- Print “Hello, !”, where is a parameter
- If parameter is not present, Print “Hello, World!”
- If parameter is not null or empty, then exception “bad arg (ex: Damien)”
Steps
Order is important, next step is also solution to previous step. (in Git Projet, Just follow classes named HelloWorld*Test
order)
- Start by developing
Print "Hello, World!"
HelloWorldTest#print_Hello_World
Complete the testHelloWorldTest#print_Hello_World
Run the test (test does not pass)HelloWorld
Write just enough implementation code to make the test pass
- Repeat with
Print "Hello, <MY NAME>!", where <MY NAME> is a parameter
HelloWorldTest#print_Hello_with_parameter
Complete the testHelloWorldTest#print_Hello_with_parameter
Run the test (test does not pass)HelloWorld
Write just enough implementation code to make the test passHelloWorldTest#*
Run all tests (tests pass)
- Repeat with
If parameter is not present, Print "Hello, World!"
HelloWorldTest#print_Hello_World_by_default
Complete the testHelloWorldTest#print_Hello_World_by_default
Run the test (test does not pass)HelloWorld
Write just enough implementation code to make the test passHelloWorldTest#*
Run all tests (tests pass)
- Refactor
HelloWorldTest
Migrate to JUnit5HelloWorld
Use Ternary operator for inline condition
- Repeat with
If parameter is not null or empty, then exception "bad arg (ex: Damien)"
HelloWorldTest#exception_bad_format
Complete the testHelloWorldTest#exception_bad_format
Run the test (test does not pass)HelloWorld
Write just enough implementation code to make the test passHelloWorldTest#*
Run all tests (tests pass)
Step 0 – Initial commit
HelloWorld.java
package com.damienfremont.blog; public class HelloWorld { public static void main(String[] args) { // TODO: NEXT: Print "Hello, World!" } }
Step 1
Start by developing Print "Hello, World!"
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit4Parent; import org.junit.Test; public class HelloWorldTest extends JUnit4Parent { @Test public void print_Hello_World() { throw new IllegalStateException("Not yet implemented!"); // TODO: NEXT OUTPUT: super.getOutput() } }
Run the test (test does not pass)

Complete the test
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit4Parent; import org.junit.Assert; import org.junit.Test; public class HelloWorld1Test extends JUnit4Parent { @Test public void print_Hello_World() { String expected = "Hello, World!"; HelloWorld1.main(null); Assert.assertEquals(expected, getOutput()); } }
Run the test (test does not pass)

Write just enough implementation code to make the test pass
HelloWorld.java
package com.damienfremont.blog; public class HelloWorld1 { public static void main(String[] args) { String text = "Hello, World!"; System.out.println(text); } }

Step 2
Repeat with Print "Hello, <MY NAME>!", where <MY NAME> is a parameter
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit4Parent; import org.junit.Assert; import org.junit.Test; public class HelloWorld2Test extends JUnit4Parent { @Test public void print_Hello_World() { String expected = "Hello, World!"; HelloWorld2.main(null); Assert.assertEquals(expected, getOutput()); } @Test public void print_Hello_with_parameter() { throw new IllegalStateException("Not yet implemented!"); // TODO: NEXT: Print "Hello, <MY NAME>!", where <MY NAME> is a parameter } }
Complete the test
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit4Parent; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.util.Arrays; import java.util.Collection; @RunWith(Parameterized.class) public class HelloWorld2Test extends JUnit4Parent { @Parameterized.Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ {"Hello, World!", "World"}, {"Hello, Damien!", "Damien"}, {"Hello, Jane!", "Jane"}, {"Hello, John!", "John"}, }); } private final String expected; private final String first; public HelloWorld2Test(String expected, String first) { this.first = first; this.expected = expected; } @Test public void print_Hello_with_parameter() { String[] args = {first}; HelloWorld2.main(args); Assert.assertEquals(expected, getOutput()); } }
Run the test (test does not pass)

Write just enough implementation code to make the test pass
HelloWorld.java
package com.damienfremont.blog; public class HelloWorld2 { public static void main(String[] args) { String text = args[0]; System.out.println(String.format("Hello, %s!", text)); } }

Step 3
Repeat with If parameter is not present, Print "Hello, World!"
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit4Parent; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.util.Arrays; import java.util.Collection; @RunWith(Parameterized.class) public class HelloWorld3Test extends JUnit4Parent { @Parameterized.Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ {"Hello, World!", "World"}, {"Hello, Damien!", "Damien"}, {"Hello, Jane!", "Jane"}, {"Hello, John!", "John"}, }); } private final String expected; private final String first; public HelloWorld3Test(String expected, String first) { this.first = first; this.expected = expected; } @Test public void print_Hello_with_parameter() { String[] args = {first}; HelloWorld3.main(args); Assert.assertEquals(expected, getOutput()); } @Test public void print_Hello_World_by_default() { throw new IllegalStateException("Not yet implemented!"); // TODO: NEXT: If parameter is not present, Print "Hello, World!" } }
Complete the test
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit4Parent; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.util.Arrays; import java.util.Collection; @RunWith(Parameterized.class) public class HelloWorld3Test extends JUnit4Parent { @Parameterized.Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ {"Hello, World!", "World"}, {"Hello, Damien!", "Damien"}, {"Hello, Jane!", "Jane"}, {"Hello, John!", "John"}, }); } private final String expected; private final String first; public HelloWorld3Test(String expected, String first) { this.first = first; this.expected = expected; } @Test public void print_Hello_with_parameter() { String[] args = {first}; HelloWorld3.main(args); Assert.assertEquals(expected, getOutput()); } @Test public void print_Hello_World_by_default() { String expected = "Hello, World!!"; HelloWorld4.main(null); Assert.assertEquals(expected, getOutput()); } }
Run the test (test does not pass)

Write just enough implementation code to make the test pass
HelloWorld.java
package com.damienfremont.blog; public class HelloWorld3 { public static void main(String[] args) { // TODO: NEXT: If parameter is not present, Print "Hello, World!" String text = args[0]; System.out.println(String.format("Hello, %s!", text)); } }

Step 4
Refactor from JUnit4 to JUnit5
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit5Parent; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class HelloWorld5Test extends JUnit5Parent { @Test void print_Hello_World_by_default() { String expected = "Hello, World!!"; HelloWorld5.main(null); Assertions.assertEquals(expected, getOutput()); } @DisplayName("print Hello with parameter (expected, parameter)") @ParameterizedTest @CsvSource({ "'Hello, Damien!', Damien", "'Hello, Jane!', Jane", "'Hello, John!', John", }) void print_Hello_with_parameter(String expected, String parameter) { String[] args = {parameter}; HelloWorld5.main(args); Assertions.assertEquals(expected, getOutput()); } }
HelloWorld.java
package com.damienfremont.blog; public class HelloWorld5 { public static void main(String[] args) { // TODO: NEXT: `If parameter is not null or empty, then exception "bad arg (ex: Damien)"` String text = hasNoArgs(args) ? "World!" : args[0]; System.out.println(String.format("Hello, %s!", text)); } private static boolean hasNoArgs(String[] args) { return args == null || (args != null && args.length == 0); } }
Run all tests (tests pass)

Step 5
Repeat with If parameter is not null or empty, then exception "bad arg (ex: Damien)"
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit5Parent; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class HelloWorld5Test extends JUnit5Parent { @Test void print_Hello_World_by_default() { String expected = "Hello, World!!"; HelloWorld5.main(null); Assertions.assertEquals(expected, getOutput()); } @DisplayName("print Hello with parameter (expected, parameter)") @ParameterizedTest @CsvSource({ "'Hello, Damien!', Damien", "'Hello, Jane!', Jane", "'Hello, John!', John", }) void print_Hello_with_parameter(String expected, String parameter) { String[] args = {parameter}; HelloWorld5.main(args); Assertions.assertEquals(expected, getOutput()); } @Test void exception_bad_format() { // TODO: NEXT: `If parameter is not null or empty, then exception "bad arg (ex: Damien)"` throw new IllegalStateException("Not yet implemented!"); } }
Complete the test
HelloWorldTest.java
package com.damienfremont.blog; import com.damienfremont.blog.utils.JUnit5Parent; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class HelloWorld6FinalTest extends JUnit5Parent { @Test void print_Hello_World_by_default() { String expected = "Hello, World!!"; HelloWorld6Final.main(null); Assertions.assertEquals(expected, getOutput()); } @DisplayName("print Hello with parameter (expected, parameter)") @ParameterizedTest @CsvSource({ "'Hello, Damien!', Damien", "'Hello, Jane!', Jane", "'Hello, John!', John", }) void print_Hello_with_parameter(String expected, String parameter) { String[] args = {parameter}; HelloWorld6Final.main(args); Assertions.assertEquals(expected, getOutput()); } @DisplayName("exception bad format (description, expected, parameter)") @ParameterizedTest @CsvSource({ "Param is empty, bad arg (ex: Damien), '' ", "Param is blanck, bad arg (ex: Damien), ' ' ", "Param is null, bad arg (ex: Damien), null" }) void exception_bad_format(String description, String expected, String parameter) { String[] args = {parameter.equals("null") ? null : parameter}; Exception e = Assertions.assertThrows(IllegalArgumentException.class, () -> { HelloWorld6Final.main(args); }); Assertions.assertEquals(expected, e.getMessage()); } }
Run the test (test does not pass)

HelloWorld.java
package com.damienfremont.blog; public class HelloWorld6Final { public static void main(String[] args) { String name = getName(args); String text = String.format("Hello, %s!", name); System.out.println(text); } private static String getName(String[] args) { if (hasNoArgs(args)) { return "World!"; } if(isNullOrEmpty(args[0])) { throw new IllegalArgumentException("bad arg (ex: Damien)"); } return args[0]; } private static boolean isNullOrEmpty(String arg) { return arg == null || arg.trim().isEmpty(); } private static boolean hasNoArgs(String[] args) { return args == null || (args != null && args.length == 0); } }
Run all tests (tests pass)

And it’s finished !!!
Conclusion
Test-driven development is not about testing.
Test-driven development is about development (and design), specifically improving the quality and design of code. The resulting unit tests are just an extremely useful by-product.