JUnit
Unit tests
Mocking
Mutation testing
Unit tests with JUnit
A popular open-source testing framework for Java:
- JUnit5 for a long time
- JUnit6 since 2025.10
Regarding updating dependencies:
- A good practice to carry out regularly
- Fixes security vulnerabilities
- Watch out for regressions and compatibility issues with other libraries
- Don’t rush to install the latest version
Dependabot:
- monitors your project’s dependencies.
- It suggests pull requests when a dependency has a new version, especially if it’s a security update.
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
Detect regressions when upgrading dependencies
3 steps of a test?
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/javadirectory
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);
}
}- @Getter : using Lombok
- Create method getAttribute()
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
- Unit Test: isolate components
- Integration testing: Do my components work together?
- Functional testing: Does my application do what the user expects?
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
}
}- Create mock
- Inject into the class
- When method is called, return directly the result
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
But does it check the behavior?
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>=
- Replace
- Checks if your tests can detect these changes
- Each your tests to fail
- Mutations are automatically seeded into your code,
- then your tests are run.
- If your tests fail then the mutation is killed
- if your tests pass then the mutation lived
- The quality of your tests can be gauged from the percentage of mutations killed