JUnit

Ludovic Deneuville

Unit tests with JUnit

A popular open-source testing framework for Java:

  • JUnit5 for a long time
  • JUnit6 since 2025.10

https://junit.org

Reminder: Unit Testing

  • Testing individual units of code
  • Verifying that each unit behaves as expected
  • Essential for detecting bugs
  • Avoid regressions
    • ↪️ Prevents new changes from breaking existing code

Test classes

  • One-to-One Relationship
  • Each class should have a corresponding test class
  • Book.java ↔︎️ BookTest.java
    • placed in the same package as the class being tested
    • but in the src/test/java directory

Example

Class Fraction

Fraction.java
public class Fraction {
    @Getter private int numerator;
    @Getter private int denominator;

    public Fraction(int numerator, int denominator) {
        if (denominator == 0)
            throw new IllegalArgumentException("Denominator cannot be zero.");
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public Fraction invert() {
        if (numerator == 0)
            throw new ArithmeticException("Cannot invert a fraction with a zero numerator.");
        return new Fraction(denominator, numerator);
    }
}

Test class

FractionTest.java
public class FractionTest {

    @Test
    void testInvertValid() {
        // GIVEN
        Fraction fraction = new Fraction(2, 3);

        // WHEN
        Fraction inverted = fraction.invert();

        // THEN
        assertEquals(3, inverted.getNumerator());
        assertEquals(2, inverted.getDenominator());
    }

    @Test
    void testInvertZeroNumerator() {
        Fraction fraction = new Fraction(0, 5);
        assertThrows(ArithmeticException.class, fraction::invert);
    }
}

Assertions

  • assertEquals(expected, actual)
  • assertTrue(condition) / assertFalse(condition)
  • assertNull(object) / assertNotNull(object)
  • assertThrows(expectedType, executable)

Annotations

  • @Test: Marks a method as a test case
  • @BeforeEach: Executes before each test method
  • @AfterEach: Executes after each test method
  • @BeforeAll: Executes once before all test methods
  • @AfterAll: Executes once after all test methods

Mocking

  • Replacing real dependencies with controlled substitutes during unit testing
  • Allows you to isolate the unit under test from external factors
  • See Mockito frameworks

Method to test

Fraction.java
@AllArgsConstructor
public class Fraction {
    private int numerator;
    private int denominator;
    private MathService mathService;

    public Fraction simplify() {
        int gcd = this.mathService.pgcd(numerator, denominator);
        return new Fraction(
                this.numerator / gcd,
                this.denominator / gcd,
                this.mathService);
    }
}

Test with mockito

FractionTest.java
class FractionTest {
    @Test
    void simplifyMockingPgcdMethodCall() {

        // GIVEN
        MathService mathService = mock(MathService.class);
        when(mathService.pgcd(6, 8)).thenReturn(2);
        Fraction f = new Fraction(6, 8, mathService);

        // WHEN
        Fraction result = f.simplify();

        // THEN
        assertEquals(3, result.getNumerator());
        assertEquals(4, result.getDenominator());
        verify(mathService).pgcd(6, 8); // Verify it was called one time
    }
}

Mutation Testing

Is it a good test ?

FractionTest.java
public class FractionTest {
    @Test
    void invert_OK() {
        // GIVEN
        Fraction f = new Fraction(2, 3);

        // WHEN
        Fraction result = f.invert();

        // THEN
        assertNotNull(result);
    }
  • executes the method
  • passes
  • gives 100% coverage

Introduce a mutation

Let’s introduce a mutation in the code:

Fraction.java
public class Fraction {
    ...

    public Fraction invert() {
        if (numerator == 0)
            throw new ArithmeticException("Cannot invert a fraction with a zero numerator.");

        return new Fraction(numerator, denominator); 
        // instead of return new Fraction(denominator, numerator);
    }
}

Does the test still pass?

  • Yes: mutant survived
  • With the first version, the mutant would have been killed

Code Coverage ≠ Test quality

Pitest

  • Evaluate the effectiveness of your tests
  • Introduces small changes (mutations) to your code
    • Replace + with - or > with >=
  • Checks if your tests can detect these changes
    • Each your tests to fail

https://pitest.org/