TDD in Java
Learn how to practice Test Driven Development using JUnit, Java's most popular testing framework.
Setting Up JUnit
In Java, we use JUnit for writing tests. JUnit 5 (also known as JUnit Jupiter) is the current standard.
Add this dependency to your pom.xml:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
If you haven't learned about Maven yet, check out the Maven Basics lesson in the Fundamentals section first.
JUnit Basics
Common Annotations
@Test: Marks a method as a test case@BeforeEach: Runs before each test method@AfterEach: Runs after each test method@BeforeAll: Runs once before all tests in the class@AfterAll: Runs once after all tests in the class
Common Assertions
import static org.junit.jupiter.api.Assertions.*;
assertEquals(expected, actual); // Check if values are equal
assertTrue(condition); // Check if condition is true
assertFalse(condition); // Check if condition is false
assertNull(object); // Check if object is null
assertNotNull(object); // Check if object is not null
assertThrows(Exception.class, () -> {}); // Check if exception is thrown
Example: Building a Calculator with TDD
Let's build a simple calculator using the TDD approach.
Step 1: Write the Test First
Create a test class in your src/test/java directory:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CalculatorTest {
@Test
public void testAddition() {
Calculator calc = new Calculator();
int result = calc.add(2, 3);
assertEquals(5, result);
}
}
Step 2: Run the Test (It Fails)
The test will fail because Calculator doesn't exist yet. This is expected in TDD - Red phase!
Error: Cannot find symbol 'Calculator'
Step 3: Write Minimum Code to Pass
Create the Calculator class in your src/main/java directory:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
Step 4: Run the Test Again (It Passes!)
Now your test should pass - Green phase! You've successfully implemented addition.
Step 5: Refactor (If Needed)
In this case, our code is already simple and clean. The Refactor phase would be where we improve code quality while keeping tests green.
Step 6: Add More Tests
Continue the cycle for other operations:
@Test
public void testSubtraction() {
Calculator calc = new Calculator();
int result = calc.subtract(5, 3);
assertEquals(2, result);
}
@Test
public void testMultiplication() {
Calculator calc = new Calculator();
int result = calc.multiply(4, 3);
assertEquals(12, result);
}
@Test
public void testDivision() {
Calculator calc = new Calculator();
int result = calc.divide(12, 3);
assertEquals(4, result);
}
@Test
public void testDivisionByZero() {
Calculator calc = new Calculator();
assertThrows(ArithmeticException.class, () -> {
calc.divide(10, 0);
});
}
Complete Calculator Implementation
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
return a / b;
}
}
Test Structure: Arrange-Act-Assert
A well-written test follows the AAA pattern:
@Test
public void testCalculateDiscount() {
// Arrange: Set up test data
PriceCalculator calculator = new PriceCalculator();
double price = 100.0;
double discountPercent = 20.0;
// Act: Execute the code being tested
double result = calculator.applyDiscount(price, discountPercent);
// Assert: Verify the result
assertEquals(80.0, result, 0.01);
}
Best Practices
-
Test One Thing: Each test should verify one specific behaviour
// Good: Tests one specific case
@Test
public void testAddPositiveNumbers() { }
// Bad: Tests multiple unrelated things
@Test
public void testAllMathOperations() { } -
Clear Test Names: Use descriptive names that explain what's being tested
// Good
@Test
public void shouldReturnZeroWhenAddingNegativeAndPositiveOfSameValue() { }
// Bad
@Test
public void test1() { } -
Keep Tests Fast: Tests should run quickly so you run them often
-
Independent Tests: Tests shouldn't depend on each other or shared state
-
Test Edge Cases: Don't just test the happy path
@Test
public void testDivisionByZero() { }
@Test
public void testNegativeNumbers() { }
@Test
public void testLargeNumbers() { }
Running Tests
From IDE
Most IDEs (IntelliJ, Eclipse, VS Code) have built-in test runners. Look for the green play button next to test methods.
From Maven
# Run all tests
mvn test
# Run tests in a specific class
mvn test -Dtest=CalculatorTest
# Run a specific test method
mvn test -Dtest=CalculatorTest#testAddition
From Command Line (Without Maven)
If you're not using Maven, you can run tests directly:
# Compile test classes
javac -cp .:junit-platform-console-standalone.jar CalculatorTest.java
# Run tests
java -jar junit-platform-console-standalone.jar --class-path . --scan-class-path
Practice Exercise
Build a StringUtils class using TDD that can:
- Reverse a string:
reverse("hello")returns"olleh" - Check if palindrome:
isPalindrome("racecar")returnstrue - Count vowels:
countVowels("hello")returns2
Remember: Write the tests first, then implement!
Getting Started
Start with this test:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class StringUtilsTest {
@Test
public void testReverse() {
StringUtils utils = new StringUtils();
String result = utils.reverse("hello");
assertEquals("olleh", result);
}
// Add more tests...
}
Next Steps
Now that you understand TDD in Java, apply these practices when building REST APIs and other applications. Tests will give you confidence to refactor and add new features without breaking existing functionality.