Welcome back to our course on Test-Driven Development (TDD) in Java using JUnit
. In our previous lesson, we introduced the fundamentals of TDD and the Red-Green-Refactor workflow. Now, we will advance our TDD skills by focusing on generalizing solutions and enhancing the complexity of our testing scenarios.
As a brief reminder, TDD involves a repetitive cycle known as Red-Green-Refactor:
- Red: Write a failing test to clarify the new functionality you aim to implement.
- Green: Develop the smallest amount of code needed to make that test pass.
- Refactor: Clean up the code, enhancing its quality while maintaining its functionality and ensuring all tests remain passing.
In this lesson, we will expand upon the sum
method, demonstrating how to generalize it while following these TDD principles.
Before we dive into coding, let's review our current setup. You are already familiar with the sum
method from Math.java
and its corresponding test in MathTest.java
:
Java1public class MathTest { 2 3 @Test 4 public void testSumAddsTwoNumbersCorrectly() { 5 Math math = new Math(); 6 int result = math.sum(2, 3); 7 assertEquals(5, result); 8 } 9}
Java1public class Math { 2 3 public int sum(int a, int b) { 4 return 5; 5 } 6}
This existing setup serves as a foundation. Now, we'll focus on expanding your understanding by generalizing the approach using TDD principles. Understanding where you've come from will help ensure future changes enhance our function without straying too far from the core logic.
To embrace the Red phase, let's introduce a new test case that will fail.
Update MathTest.java
to include more input scenarios:
Java1public class MathTest { 2 3 @Test 4 public void testSumAddsTwoNumbersCorrectly() { 5 Math math = new Math(); 6 int result = math.sum(2, 3); 7 assertEquals(5, result); 8 } 9 10 @Test 11 public void testSumShouldAddTwoMoreNumbersCorrectly() { 12 Math math = new Math(); 13 int result = math.sum(5, 6); 14 assertEquals(11, result); 15 } 16}
By including a new scenario with new inputs, this step is intentionally set to fail to define our target goal clearly.
Running this test:
Output:
error1MathTest > testSumShouldAddTwoMoreNumbersCorrectly() FAILED 2 org.opentest4j.AssertionFailedError: expected: <11> but was: <5> 3 at app//org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55) 4 at app//org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62) 5 at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150) 6 at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:145) 7 at app//org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:511) 8 at app//MathTest.sum_ShouldAddTwoMoreNumbersCorrectly(MathTest.java:17)
This failure confirms that the new functionality needs addressing.
Now, let's transition to the Green phase, where our goal is to ensure all tests pass, including our new one. We can update our sum
method to use the generic response because it is the minimal solution.
Java1public class Math { 2 3 public int sum(int a, int b) { 4 return a + b; 5 } 6}
Output:
Java1MathTest > testSumShouldAddTwoMoreNumbersCorrectly() PASSED
Success! The Green phase is complete, illustrating how effective writing minimal code to pass tests can be.
Finally, let's advance into the Refactor stage. The sum
method is very simple, so there's nothing we can make better with the implementation. We do have a lot of duplication in our tests, however.
Let's introduce "parameterized testing." A parameterized test is a single test that has several inputs and expected outputs. It makes it easy to add a lot of test cases without duplicating too much test code:
Java1public class MathTest { 2 3 @ParameterizedTest 4 @MethodSource("sumTestCases") 5 public void testSumAddsTwoNumbersCorrectly(int a, int b, int expected) { 6 Math math = new Math(); 7 int result = math.sum(a, b); 8 assertEquals(expected, result); 9 } 10 11 private static Stream<Arguments> sumTestCases() { 12 return Stream.of( 13 Arguments.of(2, 3, 5), 14 Arguments.of(5, 6, 11) 15 ); 16 } 17}
In this refactored test, we use @ParameterizedTest
, which allows a single test method to run multiple times with different sets of input data. The @MethodSource
annotation specifies the name of a static method (in this case, sumTestCases
) that provides the input data. This method returns a Stream
of Arguments
, with each Arguments
instance representing a single set of inputs and the expected output.
For now, there's no functional change — but strategically refactoring in this way offers benefits, maintaining clarity as complexity escalates over iterations.
In this lesson, we delved into using TDD to generalize solutions in Java by:
- Reviewing and examining the initial state of the
sum
method and its associated test cases. - Engaging in the Red phase by adding a new failing test to identify necessary functionality improvements.
- Advancing to the Green phase by implementing minimal code changes to pass the new test.
- Moving to the Refactor phase, where we utilized parameterized testing to reduce test duplication and improve maintainability.