TP1 : Unit Testing in Java
This first practical is your first hands-on unit testing. Basic examples will be provided and you will have to produce the unit testing.
Before the actual tests
- Build the following folder hiearchy in one of your folder:
.
├── lib
│ └── junit
│ └── jacoco
├── 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 calculor could be:
// CalculatorTest.java (in test/ 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 addition 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 33 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 5 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 time 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
User management
Difficulty: RxA simple user management system coulb 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.length() >= 8;
}
// Changes the password if the current password is correct
public boolean changePassword(String currentPassword, String newPassword) {
if (currentPassword.equals(this.password) && newPassword.length() >= 8) {
this.password = newPassword;
return true;
}
return false;
}
}
Write in a file UserTest.java the following tests:
- Test the
isValidmethod for various scenarios: valid username and password, invalid username, invalid password. - Test the
changePasswordmethod for both successful and unsuccessful password changes.
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 exception).
- Test withdrawing within the balance.
- Test withdrawing more than the balance (should throw an exception).
Once this is done, you could improve your tests. First, by setting up a default BankAccount (with an initial deposit for instance) and with reusable helper functions for instance.
Refactor: Once you have written tests, make the
ask them to refactor their code:
Make tests cleaner and more modular by using @BeforeEach to set up a default BankAccount before each test. Create reusable helper methods for common assertions.
Code Coverage
https://www.eclemma.org/jacoco/
Option 3: Using JaCoCo with Command-Line Tools (No Build Tool)
If students are not using Maven or Gradle, you can still run JaCoCo manually with the following steps: Step 1: Download JaCoCo
Download JaCoCo’s standalone version from JaCoCo Releases. Extract the .jar file somewhere accessible. Step 2: Compile the Java Code
Use the javac command to compile the code and tests:
javac -cp junit-platform-console-standalone-<version>.jar -d out/ src/*.java src/tests/*.java
Step 3: Run Tests with JaCoCo Agent
Run the tests using the JaCoCo agent to collect coverage data. You’ll need to add the jacocoagent.jar to the JVM’s classpath:
java -javaagent:/path/to/jacocoagent.jar -cp /path/to/junit-platform-console-standalone-<version>.jar:out org.junit.platform.console.ConsoleLauncher --scan-classpath
This will generate a coverage file called jacoco.exec.
Step 4: Generate Report
Once you have the jacoco.exec file, you can generate the HTML report by running JaCoCo’s report generator:
java -jar jacococli.jar report jacoco.exec --classfiles out --sourcefiles src --html report
The generated report will be saved in the report/ directory. Interpreting the Coverage Results
When analyzing the report, JaCoCo will provide:
Class-level metrics: Shows which classes have been covered and which haven’t. Method-level metrics: Coverage per method within the classes. Line and Branch coverage: Helps identify if certain logical branches (like if statements) have been skipped by tests.
Code Coverage Targets:
80%-90% line coverage is often considered good. 100% line coverage does not necessarily mean the code is bug-free, but it ensures that all parts of the code have been executed by the tests.