Unit Testing in Java
This is your first hands-on practical with unit testing. A complete example test is provided for the calculator only; for the other classes, you will have to write the tests yourself.
Objectives
- Write and run JUnit 5 tests from the command line, without an IDE’s built-in test runner.
- Use core assertions (
assertEquals,assertThrows, …) and organize tests with@BeforeEach. - Measure test coverage with JaCoCo and interpret line/branch/method coverage.
Before the actual tests
Difficulty: Easy- Build the following folder hierarchy in one of your folders:
.
├── lib
├── out
├── src
└── tests
- Download
JUnitwith the following command. Ensure that the package is now in yourlibfolder
curl https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.9.3/junit-platform-console-standalone-1.9.3.jar -o lib/junit-platform-console-standalone.jar
Calculator
Difficulty: EasyBelow is the code for a very simple calculator:
// Calculator.java (should be placed in your src/ folder)
public class Calculator {
// Adds two numbers
public int add(int a, int b) {
return a + b;
}
// Subtracts the second number from the first
public int subtract(int a, int b) {
return a - b;
}
// Multiplies two numbers
public int multiply(int a, int b) {
return a * b;
}
// Divides the first number by the second
// Should throw an ArithmeticException if division by zero is attempted
public int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Division by zero is not allowed");
}
return a / b;
}
}
A minimal test file for the calculator could be:
// CalculatorTest.java (in tests/ folder)
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3), "2 + 3 should equal 5");
}
@Test
public void testDivideByZero() {
Calculator calc = new Calculator();
assertThrows(ArithmeticException.class, () -> calc.divide(10, 0), "Division by zero should throw an exception");
}
}
Two basic tests are defined, one for the add method, one for divide.
Compile your files with the following command:
javac -cp lib/junit-platform-console-standalone.jar -d out/ src/*.java tests/*.java
And run your two tests with:
java -jar lib/junit-platform-console-standalone.jar --class-path out/ --scan-classpath
The output should be something like this showing that 2 tests have been executed and they both have succeeded:
Thanks for using JUnit! Support its development at https://junit.org/sponsoring
╷
├─ JUnit Jupiter ✔
│ └─ CalculatorTest ✔
│ ├─ testDivideByZero() ✔
│ └─ testAdd() ✔
├─ JUnit Vintage ✔
└─ JUnit Platform Suite ✔
Test run finished after 21 ms
[ 4 containers found ]
[ 0 containers skipped ]
[ 4 containers started ]
[ 0 containers aborted ]
[ 4 containers successful ]
[ 0 containers failed ]
[ 2 tests found ]
[ 0 tests skipped ]
[ 2 tests started ]
[ 0 tests aborted ]
[ 2 tests successful ]
[ 0 tests failed ]
Now it is your turn to write tests:
- Test the add, subtract, multiply, and divide methods for typical values.
- Test the divide method for division by zero and ensure it throws the appropriate exception. You should use
assertThrows - Test integer division with a non-exact result (e.g.
divide(7, 2)): what value do you expect, and why?
User management
Difficulty: RxA simple user management system could be described by the following file:
// User.java
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
// Returns true if the username and password are valid
public boolean isValid() {
return username != null && !username.isEmpty() && password != null && password.length() >= 8;
}
// Changes the password if the current password is correct
public boolean changePassword(String currentPassword, String newPassword) {
if (this.password != null && this.password.equals(currentPassword)
&& newPassword != null && newPassword.length() >= 8) {
this.password = newPassword;
return true;
}
return false;
}
}
Write in a file UserTest.java the following tests:
The validation rules are: the username must be non-null and non-empty (a whitespace-only username is accepted by this simple implementation), and the password must be non-null and at least 8 characters long.
- Test the
isValidmethod for various scenarios: valid username and password, invalid username (nulland empty), invalid password (nulland too short). - Test the
changePasswordmethod for both successful and unsuccessful password changes. Note thatfalseis returned in two different situations (wrong current password, or new password shorter than 8 characters): write one test for each.
Bank account management
Difficulty: RxConsidering a simple bank account management:
// BankAccount.java
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
// Returns the current balance
public double getBalance() {
return balance;
}
// Deposits money into the account, but throws an IllegalArgumentException if the amount is negative
public void deposit(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Cannot deposit negative amounts");
}
balance += amount;
}
// Withdraws money from the account if enough balance is available
// Throws an IllegalArgumentException if the amount is negative
// Throws an IllegalStateException if the amount exceeds the balance
public void withdraw(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Cannot withdraw negative amounts");
}
if (amount > balance) {
throw new IllegalStateException("Insufficient balance");
}
balance -= amount;
}
}
Write unit tests for the BankAccount class.
- Test depositing positive amounts.
- Test depositing a negative amount (should throw an
IllegalArgumentException). - Test withdrawing within the balance.
- Test withdrawing more than the balance (should throw an
IllegalStateException). - Test the edge cases: depositing or withdrawing
0, and withdrawing exactly the balance.
Balances are doubles, so use the overload with a tolerance: assertEquals(150.0, account.getBalance(), 0.001).
Once this is done, improve your tests by setting up a default BankAccount (with an initial deposit) using @BeforeEach, and by extracting reusable helper methods for common assertions.
Code Coverage
Difficulty: RxCode coverage measures how much of your source code is exercised by your tests. We use JaCoCo with command-line tools.
Step 1: Download JaCoCo
Download a JaCoCo release (here 0.8.15; use a recent version, as older ones cannot read class files compiled by recent JDKs, see the JaCoCo releases page for other versions), unzip it, and copy jacocoagent.jar and jacococli.jar from its lib/ folder into your lib/jacoco/ folder:
mkdir -p lib/jacoco
curl -L https://repo1.maven.org/maven2/org/jacoco/jacoco/0.8.15/jacoco-0.8.15.zip -o jacoco.zip
unzip -o jacoco.zip -d jacoco-dist
cp jacoco-dist/lib/jacocoagent.jar jacoco-dist/lib/jacococli.jar lib/jacoco/
Step 2: Compile the Java code
javac -cp lib/junit-platform-console-standalone.jar -d out/ src/*.java tests/*.java
Step 3: Run tests with the JaCoCo agent
java -javaagent:lib/jacoco/jacocoagent.jar \
-cp lib/junit-platform-console-standalone.jar:out \
org.junit.platform.console.ConsoleLauncher --scan-classpath
This generates a coverage data file jacoco.exec.
On Windows, the classpath separator is ; instead of : (-cp lib/junit-platform-console-standalone.jar;out).
Step 4: Generate the HTML report
java -jar lib/jacoco/jacococli.jar report jacoco.exec \
--classfiles out --sourcefiles src --html report
Open report/index.html in a browser to view coverage results.
out/, report/, jacoco.exec, jacoco.zip and jacoco-dist/ are generated files: do not commit them (add them to your .gitignore).
Interpreting coverage results
JaCoCo reports coverage at three levels:
- Line coverage: which lines were executed at least once
- Branch coverage: which conditional branches (e.g., both sides of an
if) were exercised - Method coverage: which methods were called at all
80–90% line coverage is a commonly cited target for a well-tested project. 100% line coverage does not guarantee correctness — it only confirms that all lines ran, not that they ran correctly.