JUnit 5 Migration Guide
📋 Tracking Issue: Issue #225 - Migrate from JUnit 4 to JUnit 5
Overview
The project is gradually migrating from JUnit 4 to JUnit 5 (Jupiter). Both frameworks can coexist during the transition, allowing us to:
- ✅ Write new tests in JUnit 5
- ✅ Keep existing JUnit 4 tests running
- ✅ Migrate tests incrementally at our own pace
- ✅ Avoid big-bang migration risks
Current Status
✅ Phase 1 Complete - JUnit 5 Support Enabled
See Issue #225 for detailed progress tracking.
- Maven Surefire 2.22.2 configured to run both JUnit 4 and JUnit 5 tests
- JUnit Vintage Engine included for backward compatibility
- First JUnit 5 tests:
ProtoComprehensiveTestandTextProtoLoaderTest
Pilot Tests
The following tests serve as examples for JUnit 5 migration:
ProtoComprehensiveTest.java- Comprehensive tests forProto.javautilityTextProtoLoaderTest.java- Comprehensive tests forTextProtoLoader.javautility
These tests demonstrate JUnit 5 best practices and can be used as templates for migrating other tests.
JUnit 4 vs JUnit 5 Comparison
Imports
JUnit 4:
import org.junit.Test;
import org.junit.Before;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.AfterClass;
import static org.junit.Assert.*;
JUnit 5:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;
import static org.junit.jupiter.api.Assertions.*;
Annotations
| JUnit 4 | JUnit 5 | Purpose |
|---|---|---|
@Test | @Test | Mark test method |
@Before | @BeforeEach | Run before each test |
@After | @AfterEach | Run after each test |
@BeforeClass | @BeforeAll | Run once before all tests |
@AfterClass | @AfterAll | Run once after all tests |
@Ignore | @Disabled | Skip test |
@Test(expected = Exception.class) | assertThrows(Exception.class, () -> {...}) | Expect exception |
@Test(timeout = 1000) | @Timeout(1) or assertTimeout(...) | Timeout |
Assertions
JUnit 4:
import static org.junit.Assert.*;
assertEquals(expected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(object);
assertNotNull(object);
assertSame(expected, actual);
assertArrayEquals(expected, actual);
JUnit 5:
import static org.junit.jupiter.api.Assertions.*;
// Same methods, but with optional message as last parameter
assertEquals(expected, actual);
assertEquals(expected, actual, "Custom message");
assertTrue(condition);
assertFalse(condition);
assertNull(object);
assertNotNull(object);
assertSame(expected, actual);
assertArrayEquals(expected, actual);
// New assertions
assertAll(...); // Group multiple assertions
assertThrows(Exception.class, () -> {...}); // Expect exception
assertTimeout(Duration.ofSeconds(1), () -> {...}); // Timeout
Exception Testing
JUnit 4:
@Test(expected = IllegalArgumentException.class)
public void testInvalidInput() {
myMethod(invalidInput);
}
JUnit 5:
@Test
public void testInvalidInput() {
assertThrows(IllegalArgumentException.class, () -> {
myMethod(invalidInput);
});
}
// Can also verify exception message
@Test
public void testInvalidInputWithMessage() {
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
myMethod(invalidInput);
});
assertEquals("Expected error message", exception.getMessage());
}
Migration Strategy
Phase 1: Enable JUnit 5 Support ✅ COMPLETE
- Add JUnit 5 dependencies to
pom.xml - Add JUnit Vintage Engine for JUnit 4 compatibility
- Upgrade maven-surefire-plugin to 2.22.2
- Remove JUnit 5 exclusions from tess4j dependency
- Create pilot tests (
ProtoComprehensiveTest,TextProtoLoaderTest) - Verify both JUnit 4 and JUnit 5 tests run successfully
Phase 2: Write New Tests in JUnit 5 (Current)
Guidelines for new tests:
- ✅ Use JUnit 5 for all new test classes
- ✅ Follow pilot test patterns
- ✅ Use modern assertions and features
- ✅ Document migration examples
New tests to write in JUnit 5:
- Tests for Issue #224 enhancements (Proto utility methods)
- Tests for Project Metadata feature
- Tests for any new features going forward
Phase 3: Gradual Migration of Existing Tests (Future)
Priority for migration:
- High: Tests that are actively being modified
- Medium: Tests for core utilities and frequently changed code
- Low: Tests that rarely change and work fine
Migration approach:
- Migrate tests opportunistically when touching related code
- No rush to migrate working JUnit 4 tests
- Focus on new development using JUnit 5
Phase 4: Complete Migration (Long-term)
Once all tests are migrated:
- Remove JUnit 4 dependency
- Remove JUnit Vintage Engine
- Update documentation
Writing New Tests in JUnit 5
Template
package org.codetricks.construction.code.assistant.mypackage;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
/**
* JUnit 5 test class for MyClass.
*/
@DisplayName("MyClass Tests")
public class MyClassTest {
private MyClass myClass;
@BeforeEach
public void setUp() {
myClass = new MyClass();
}
@AfterEach
public void tearDown() {
// Cleanup if needed
}
@Test
@DisplayName("Should do something when condition is met")
public void testSomething() {
// Arrange
String input = "test input";
// Act
String result = myClass.doSomething(input);
// Assert
assertNotNull(result);
assertEquals("expected output", result);
}
@Test
@DisplayName("Should throw exception for invalid input")
public void testInvalidInput() {
assertThrows(IllegalArgumentException.class, () -> {
myClass.doSomething(null);
});
}
}
Best Practices
- Use
@DisplayNamefor readable test descriptions - Use
assertThrowsinstead of@Test(expected = ...) - Use
assertAllto group related assertions - Use parameterized tests for testing multiple inputs
- Use nested test classes for better organization
Parameterized Tests Example
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.CsvSource;
@ParameterizedTest
@ValueSource(strings = {"A-1", "R-2", "B", "M"})
@DisplayName("Should parse valid IBC codes")
public void testParseValidIbcCodes(String ibcCode) {
IbcOccupancyGroup occupancy = IbcOccupancyGroupUtils.fromIbcCode(ibcCode);
assertNotNull(occupancy);
assertNotEquals(IbcOccupancyGroup.UNKNOWN, occupancy);
}
@ParameterizedTest
@CsvSource({
"R-2, Residential",
"A-1, Assembly",
"B, Business"
})
@DisplayName("Should return correct display names")
public void testDisplayNames(String ibcCode, String expectedCategory) {
IbcOccupancyGroup occupancy = IbcOccupancyGroupUtils.fromIbcCode(ibcCode);
String displayName = IbcOccupancyGroupUtils.getDisplayName(occupancy);
assertTrue(displayName.contains(expectedCategory));
}
Nested Tests Example
import org.junit.jupiter.api.Nested;
@DisplayName("ProjectMetadata Tests")
public class ProjectMetadataTest {
@Nested
@DisplayName("Creation Tests")
class CreationTests {
@Test
@DisplayName("Should create metadata with all fields")
public void testCreateComplete() {
// Test creation
}
}
@Nested
@DisplayName("Update Tests")
class UpdateTests {
@Test
@DisplayName("Should update project name")
public void testUpdateName() {
// Test update
}
}
}
Running Tests
Run All Tests (JUnit 4 + JUnit 5)
export JAVA_HOME=/usr/lib/jvm/temurin-23-jdk-arm64 && mvn test
Run Specific Test Class
# JUnit 5 test
export JAVA_HOME=/usr/lib/jvm/temurin-23-jdk-arm64 && mvn test -Dtest=ProtoComprehensiveTest
# JUnit 4 test (still works!)
export JAVA_HOME=/usr/lib/jvm/temurin-23-jdk-arm64 && mvn test -Dtest=ProtoTest
Run Tests by Pattern
# Run all Proto-related tests
export JAVA_HOME=/usr/lib/jvm/temurin-23-jdk-arm64 && mvn test -Dtest=Proto*Test
# Run all tests in a package
export JAVA_HOME=/usr/lib/jvm/temurin-23-jdk-arm64 && mvn test -Dtest=org.codetricks.construction.code.assistant.io.*Test
Benefits of JUnit 5
1. Better Assertions
// Group multiple assertions - all run even if one fails
assertAll("user properties",
() -> assertEquals("John", user.getFirstName()),
() -> assertEquals("Doe", user.getLastName()),
() -> assertEquals("john.doe@example.com", user.getEmail())
);
2. Improved Exception Testing
// Can verify exception message and cause
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
myMethod(invalidInput);
});
assertTrue(exception.getMessage().contains("expected text"));
assertNotNull(exception.getCause());
3. Parameterized Tests
// Test multiple inputs without code duplication
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 5, 8, 13})
public void testFibonacci(int input) {
assertTrue(isFibonacci(input));
}
4. Dynamic Tests
// Generate tests at runtime
@TestFactory
Stream<DynamicTest> dynamicTests() {
return Stream.of("A-1", "R-2", "B")
.map(code -> dynamicTest("Test " + code, () -> {
assertNotNull(IbcOccupancyGroupUtils.fromIbcCode(code));
}));
}
5. Better Display Names
@DisplayName("ProjectMetadata Service Tests")
public class ProjectMetadataServiceTest {
@Test
@DisplayName("Should create metadata with valid project name")
public void testCreateWithValidName() {
// Test reports show: "Should create metadata with valid project name"
// Instead of: "testCreateWithValidName"
}
}
Troubleshooting
Tests Not Running
If tests don't run, check:
# Verify JUnit 5 dependencies
mvn dependency:tree | grep junit
# Should see:
# - junit:junit:4.13.2 (JUnit 4)
# - org.junit.jupiter:junit-jupiter-api:5.11.3 (JUnit 5)
# - org.junit.vintage:junit-vintage-engine:5.11.3 (JUnit 4 compatibility)
Surefire Version Issues
Ensure maven-surefire-plugin is 2.22.0 or higher:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
Mixed Test Results
If you see unexpected behavior:
# Clean and rebuild
export JAVA_HOME=/usr/lib/jvm/temurin-23-jdk-arm64 && mvn clean test
# Run with debug output
export JAVA_HOME=/usr/lib/jvm/temurin-23-jdk-arm64 && mvn test -X
Migration Checklist
When migrating a JUnit 4 test to JUnit 5:
- Update imports (
org.junit→org.junit.jupiter.api) - Update annotations (
@Before→@BeforeEach, etc.) - Update assertions (
Assert.assertEquals→Assertions.assertEquals) - Convert
@Test(expected = ...)toassertThrows(...) - Convert
@Test(timeout = ...)toassertTimeout(...)or@Timeout - Add
@DisplayNamefor better test reports - Consider using parameterized tests for multiple inputs
- Run tests to verify migration
- Update any test documentation
Progress Tracking
Track migration progress in Issue #225.
Current Stats:
- ✅ JUnit 5 Tests: 30 tests (ProtoComprehensiveTest: 16, TextProtoLoaderTest: 14)
- ✅ JUnit 4 Tests: ~84 tests (legacy)
- 📊 Migration Progress: ~26% (30/114 tests)
Technical Background
Why Did We Need to Upgrade Maven Surefire?
Maven Surefire Plugin is the test execution engine for Maven. Our old version (2.12.4) only supported JUnit 4.
| Version | JUnit 4 | JUnit 5 | Both Together |
|---|---|---|---|
| 2.12.4 (old) | ✅ | ❌ | ❌ |
| 2.22.2 (new) | ✅ | ✅ | ✅ |
Upgrading to 2.22.2 allows us to run both JUnit 4 and JUnit 5 tests in the same build.
Why Did We Remove JUnit 5 Exclusions from tess4j?
tess4j (Tesseract OCR wrapper) has a transitive dependency that brought in JUnit 5:
tess4j:5.9.0 → pdfbox-tools:3.0.1 → junit-jupiter:5.11.3
Before: We excluded JUnit 5 to avoid conflicts with our JUnit 4-only setup.
Now: We removed the exclusions because:
- We want JUnit 5 for modern tests
- Surefire 2.22.2 handles both frameworks
- JUnit Vintage Engine keeps JUnit 4 tests working
See Issue #225 Technical Details for complete explanation.
See Also
- Issue #225 - Migration tracking issue with technical details
- JUnit 5 User Guide
- JUnit 5 Migration Guide (Official)
- Proto Utility Tests:
src/test/java/org/codetricks/construction/code/assistant/io/ProtoComprehensiveTest.java- Example JUnit 5 tests - Developer Playbook