Skip to main content
Software Maintenance
Software Maintenance
ISIA
Rx 2h

TP1 : Unit Testing in Java

Info

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

  1. Build the following folder hiearchy in one of your folder:
.
├── lib
   └── junit
   └── jacoco
├── out
├── src
└── tests
  1. Download JUnit with the following command. Ensure that the package is now in your lib folder
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: Easy

Below is the code for a very simple calculator:

Calculator.java
// 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
// 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:

  1. Test the add, subtract, multiply, and divide methods for typical values.
  2. Test the divide method for division by zero and ensure it throws the appropriate exception. You should use assertThrows

User management

Difficulty: Rx

A simple user management system coulb be described by the following file:

User.java
// 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:

  1. Test the isValid method for various scenarios: valid username and password, invalid username, invalid password.
  2. Test the changePassword method for both successful and unsuccessful password changes.

Bank account management

Difficulty: Rx

Considering a simple bank account management:

BankAccount.java
// 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.

  1. Test depositing positive amounts.
  2. Test depositing a negative amount (should throw an exception).
  3. Test withdrawing within the balance.
  4. 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.