Time to write your first real test. By the end of this lesson, you will have a working test class that you can run. Let us start with a simple example and build from there.
The Class We Want to Test
Let us say we have a Calculator class:
package com.example;
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;
}
}
This is a basic calculator with four operations. We want to make sure each method works correctly. Let us write tests for them.
Writing the Test Class
Create a new test file in the test directory:
package com.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void shouldAddTwoNumbers() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3));
}
@Test
void shouldSubtractTwoNumbers() {
Calculator calc = new Calculator();
assertEquals(1, calc.subtract(3, 2));
}
@Test
void shouldMultiplyTwoNumbers() {
Calculator calc = new Calculator();
assertEquals(6, calc.multiply(2, 3));
}
@Test
void shouldDivideTwoNumbers() {
Calculator calc = new Calculator();
assertEquals(2, calc.divide(6, 3));
}
@Test
void shouldThrowExceptionWhenDividingByZero() {
Calculator calc = new Calculator();
assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
}
}
Anatomy of a Test Method
Let us break down what is happening in each test method:
@Test <-- Annotation that marks this as a test
void shouldAddTwoNumbers() { <-- Method name describes the expected behavior
Calculator calc = new Calculator(); <-- Arrange: set up the test
assertEquals(5, calc.add(2, 3)); <-- Act & Assert: run and verify
}
Every test method follows the AAA pattern:
- Arrange - Set up the test data and objects
- Act - Execute the method you want to test
- Assert - Verify the result is what you expected
Running Your Tests
Run the tests from the command line:
# Maven
mvn test
# Gradle
gradle test
Or from your IDE by right-clicking the test class and selecting "Run."
If all tests pass, you will see something like:
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
If a test fails, JUnit tells you exactly which test failed and why. For example:
org.opentest4j.AssertionFailedError:
expected: <5> but was: <6>
at CalculatorTest.shouldAddTwoNumbers(CalculatorTest.java:10)
Test Method Naming
Notice how the test methods are named. Good test names describe the scenario and the expected behavior. Here are some naming conventions people use:
// Style 1: shouldXxx
void shouldReturnSumWhenGivenTwoNumbers() { }
// Style 2: descriptive
void add_withPositiveNumbers_returnsSum() { }
// Style 3: testXxx
void testAddition() { }
// Style 4: givenXxxWhenYyyThenZzz
void givenTwoNumbers_whenAdded_thenReturnsSum() { }
There is no single "right" way. Pick a style and be consistent. The key is that when a test fails, the name should tell you what went wrong.
Your Turn
Try this exercise. Create a StringHelper class with these methods:
public class StringHelper {
public boolean isPalindrome(String str) {
String reversed = new StringBuilder(str).reverse().toString();
return str.equalsIgnoreCase(reversed);
}
public int countVowels(String str) {
int count = 0;
for (char c : str.toLowerCase().toCharArray()) {
if ("aeiou".indexOf(c) != -1) {
count++;
}
}
return count;
}
public String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
}
Write tests for each method. Think about edge cases: what happens with empty strings? Null values? Single characters? This is how you build a solid test suite.