Process for converting existing JUnit 4 tests to JUnit 5 + AssertJ. The
target style for the migrated code is defined in
writing-tests.md — read that first. This document adds
the JUnit → AssertJ mapping, guidance for a bulk first pass, and the
post-migration audit that catches what a bulk assertEquals → assertThat
rewrite leaves behind.
| JUnit | AssertJ |
|---|---|
assertEquals(expected, actual) |
assertThat(actual).isEqualTo(expected) |
assertSame(expected, actual) |
assertThat(actual).isSameAs(expected) |
assertArrayEquals(expected, actual) |
assertThat(actual).isEqualTo(expected) or containsExactly(...) for primitives |
assertEquals(n, collection.size()) |
assertThat(collection).hasSize(n) |
assertEquals(collection.size(), other.size()) |
assertThat(collection).hasSameSizeAs(other) |
assertTrue(collection.contains(x)) |
assertThat(collection).contains(x) |
assertFalse(collection.contains(x)) |
assertThat(collection).doesNotContain(x) |
assertTrue(collection.isEmpty()) |
assertThat(collection).isEmpty() |
assertFalse(collection.isEmpty()) |
assertThat(collection).isNotEmpty() |
assertEquals(0, command.run()) |
assertThat(command.run()).isZero() |
assertEquals(0, file.length()) |
assertThat(file.length()).isZero() |
assertTrue(0 < file.length()) |
assertThat(file.length()).isPositive() |
assertTrue(file.exists()) |
assertThat(file).exists() |
@Test(expected = X.class) / assertThrows(X.class, …) |
assertThatThrownBy(…).isInstanceOf(X.class).hasMessage(…) |
assertTrue(condition) |
assertThat(condition).isTrue() |
assertNull(x) |
assertThat(x).isNull() |
assertTrue(true) after successful run |
assertThatCode(() -> action()).doesNotThrowAnyException() |
assert (exitCode == 0) |
assertThat(exitCode).isZero() |
Assume.assumeTrue(condition) (JUnit 4) |
assumeThat(condition).isTrue() (AssertJ) |
Assumptions.assumeTrue(condition) (JUnit 5) |
assumeThat(condition).isTrue() (AssertJ) |
assertEquals(Collections.emptyList(), list) |
assertThat(list).isEmpty() |
assertEquals(List.of(a, b, …), list) |
assertThat(list).containsExactly(a, b, …) |
assertEquals(otherList, list) (ordered) |
assertThat(list).containsExactlyElementsOf(otherList) |
TestUtils.assertThrows(…) |
assertThatThrownBy(…).isInstanceOf(…).hasMessage(…) |
try/catch + fail for expected exception |
assertThatThrownBy(…) or assertThatCode(…).doesNotThrowAnyException() |
See writing-tests.md for the full rationale behind each
target form (actual-vs-expected order, collection/optional/iterator assertions,
@TempDir, assumptions, and exception style).
Many legacy tests used helpers like assertThrows(String description, Class<? extends Exception> expected, Runnable r) that only verified the exception class. The first String parameter was a test description (shown when no exception was thrown), not exception.getMessage().
When converting these helpers:
- Replace with
assertThatThrownBy+isInstanceOf. - Also add
hasMessage/hasMessageContainingusing the actual message from the productionthrowstatement or a test run — do not copy the old description string intohasMessage.
// Legacy helper — description is NOT the exception message
assertThrows(
"Should not allow TIME_MICROS with " + primitive,
IllegalArgumentException.class,
() -> new AvroSchemaConverter().convert(message(type)));
// Wrong migration — old description used as message
assertThatThrownBy(() -> new AvroSchemaConverter().convert(message(type)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Should not allow TIME_MICROS with " + primitive);
// Correct migration — message from the throw site
assertThatThrownBy(() -> new AvroSchemaConverter().convert(message(type)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("…"); // actual text from throw new IllegalArgumentException(…)Start with a syntax-only bulk rewrite (search-and-replace or a scripted pass) that converts a subset of JUnit Assert calls and swaps imports. This is a mechanical first pass only — it does not produce finished AssertJ style on its own.
| Input | Output |
|---|---|
assertEquals(expected, actual) |
assertThat(actual).isEqualTo(expected) |
assertArrayEquals(expected, actual) |
assertThat(actual).isEqualTo(expected) |
assertNull / assertNotNull |
isNull / isNotNull |
assertTrue / assertFalse |
isTrue / isFalse on the same boolean expression |
assertSame / assertNotSame |
isSameAs / isNotSameAs |
Assert.fail(…) |
AssertJ fail(…) |
JUnit Assert / Hamcrest imports |
AssertJ static imports (when Assert was present) |
Everything in writing-tests.md that is not in the table above — including but not limited to:
| Category | Left behind by script | Target |
|---|---|---|
| Exceptions | catchThrowable + separate assertThat(caught) |
assertThatThrownBy + isInstanceOf + hasMessage* |
| Exceptions | @Test(expected), JUnit assertThrows, TestUtils.assertThrows, try/catch/fail |
assertThatThrownBy + isInstanceOf + hasMessage* |
| Exceptions | assertThatThrownBy with only isInstanceOf |
Add hasMessage / hasMessageContaining from the throw site |
| Exceptions | Old assertThrows(description, …) description copied into hasMessage |
Real exception text, not the test description |
| Collections | isEqualTo(Collections.emptyList()), isEqualTo(List.of(…)), isEqualTo(otherList) |
isEmpty(), containsExactly(…), containsExactlyElementsOf(…) |
| Collections | assertThat(x.contains(…)).isTrue() / isEmpty().isTrue() / size().isEqualTo(n) |
contains, isEmpty, hasSize, hasSameSizeAs, … |
| Optional | assertThat(opt.isPresent()).isTrue() / isFalse() |
assertThat(opt).isPresent() / isEmpty() |
| Numeric | assertThat(comparison).isTrue() / isFalse() |
isZero, isPositive, isGreaterThan, hasSizeLessThan, … |
| Numeric | assertThat(x).isEqualTo(0) on integral results |
assertThat(x).isZero() |
| Files | assertThat(file.exists()).isTrue() |
assertThat(file).exists() |
| Strings | assertThat(x.toString()) |
assertThat(x).asString() |
| Assumptions | Assume.assumeTrue / Assumptions.assumeTrue / org.junit.jupiter.api.Assumptions |
AssertJ assumeThat |
| Temp dirs | @Rule TemporaryFolder, package-private @TempDir |
@TempDir private Path tempDir (+ accessor for subclasses) |
| Temp paths | toFile().getAbsolutePath(), new File(tempDir.toFile(), …), create+delete before writer |
tempDir.resolve(…) + new Path(….toUri()) |
| Ordering | assertThat(a.compareTo(b) < 0).isTrue() |
assertThat(a).isLessThan(b) / isEqualByComparingTo |
| Ordering | assertThat(compare(a, b)).isPositive() etc. |
Keep, but migrate from assertTrue(compare(…) > 0) |
| Type / reference | instanceof / == / || wrapped in isTrue() |
isInstanceOf, isSameAs, isIn |
| Iterators | Custom drain helpers (assertIteratorEquals, …) |
assertThat(it).toIterable().containsExactly(…) |
| Iterators | assertThat(it.hasNext()).isFalse() / assertFalse(it.hasNext()) |
assertThat(it).isExhausted() |
| Streams | assertThat(result).isEqualTo(stream.collect(…)) |
containsExactlyElementsOf(…) after collecting |
| Maps / sets | hasSize + containsKey / contains on same subject |
containsOnlyKeys / containsExactlyInAnyOrder |
| No exception | assertTrue(true), empty try/catch |
assertThatCode(…).doesNotThrowAnyException() |
| Style | .as("…") on assertThatThrownBy |
Omit |
| Style | Block lambda with one statement in assertThatThrownBy |
Expression lambda or method reference |
| Bugs | assertThat(expr.method()).isEqualTo(…) — AssertJ chained on the value |
assertThat(expr).method(…) — chain on the assertion |
A bulk rewrite is not sufficient. After any bulk assertEquals → assertThat edit, always run the post-migration audit below before considering a module done.
Run these checks on the migrated tree. Review every hit — some patterns are heuristics and may have false positives (e.g. isEqualTo(0) on a non-numeric field).
Replace <module-test> with the module's src/test directory (e.g. parquet-hadoop/src/test).
rg 'import static org\.junit\.Assert|import org\.junit\.Assert|org\.hamcrest|org\.junit\.Assume|Assumptions\.assumeTrue' <module-test># Not yet migrated to AssertJ
rg '@Test\(expected|TestUtils\.assertThrows|\bassertThrows\(' <module-test>
rg 'catchThrowable\(' <module-test>
# Type-only exception checks (missing message assertion)
rg 'assertThatThrownBy\([^;]+\)\s*\.isInstanceOf\([^)]+\)\s*;' <module-test>
# Invalid or discouraged
rg 'assertThatThrownBy\([^)]+\)\s*\.as\(' <module-test>
rg 'hasMessage\(\(String\) null\)|hasMessage\(null\)' <module-test>
# try/catch used only to assert an exception (manual review)
rg -U 'catch\s*\([^)]+\)\s*\{[^}]*fail\(' <module-test>When converting TestUtils.assertThrows or JUnit assertThrows, add hasMessage from the production throw statement — not from the old test description string.
rg 'isEqualTo\(Collections\.emptyList\(\)\)|isEqualTo\(List\.of\(|isEqualTo\(java\.util\.List\.of\(' <module-test>
rg '\.contains\([^)]+\)\.(isTrue|isFalse)\(\)' <module-test>
rg '\.isEmpty\(\)\.(isTrue|isFalse)\(\)' <module-test>
rg 'assertThat\([^)]+\.size\(\)\)\.' <module-test>
rg '\.hasSize\([^)]+\.(length|size)\(\)\)' <module-test>
rg 'assertIteratorEquals|assertAllRowsEqual' <module-test>
rg 'assertThat\([^)]+\.isPresent\(\)\)\.(isTrue|isFalse)' <module-test>rg 'assertThat\([^)]+\.hasNext\(\)\)\.(isTrue|isFalse)' <module-test>
rg 'assertFalse\([^)]+\.hasNext\(\)\)' <module-test>
rg 'assertThat\([^)]+\)\.isEqualTo\([^)]*\.stream\(\)' <module-test>
rg 'assertThat\([^)]+\.length\)\.isEqualTo' <module-test>rg 'assertThat\([^)]*[<>!=]=?[^)]*\)\.(isTrue|isFalse)\(\)' <module-test>
rg 'assertThat\([^)]+\)\.isEqualTo\(0\)|assertThat\([^)]+\)\.isEqualTo\(0L\)' <module-test>Manually fix hits such as assertThat(file.length()).isEqualTo(0) → isZero(), assertThat(x).isPositive(), etc.
rg '\.exists\(\)\)\.(isTrue|isFalse)|assertTrue\([^)]*\.exists\(\)' <module-test>
rg 'assertThat\([^)]+\.toString\(\)\)' <module-test>
rg 'Assume\.assumeTrue|Assumptions\.assumeTrue|org\.junit\.jupiter\.api\.Assumptions' <module-test>
rg -U '@TempDir\s*\n\s*(public |protected )?[A-Za-z]' <module-test>
rg 'TemporaryFolder|@Rule.*[Tt]emp' <module-test>
rg 'tempDir\.toFile\(\)|new File\(tempDir\.toFile\(\)|toFile\(\)\.getAbsolutePath\(\)' <module-test>
rg 'Files\.createFile\(tempDir' <module-test>
rg 'AvroDirectWriterTest' <module-test>rg 'compareTo\([^)]+\)\s*(<|>|==|!=)' <module-test>
rg 'assertThat\([^)]*instanceof[^)]*\)\.(isTrue|isFalse)' <module-test>
rg 'assertThat\([^)]*==[^)]*\)\.isTrue' <module-test>
rg 'assertThat\([^)]*\|\|[^)]*\)\.isTrue' <module-test># AssertJ API wrongly nested inside assertThat(...) — often a compile error or silent wrong subject
rg 'assertThat\([^)]+\.(isEqualTo|isNull|isNotNull|isTrue|isFalse|contains|hasSize)\(' <module-test>
# Literal or constant as subject (heuristic — review each hit)
rg 'assertThat\((true|false|null|\d+|"[^"]*")\)' <module-test>rg 'assertTrue\(true\)|assertThat\(true\)\.isTrue' <module-test>
rg '\bassert\s+' <module-test>- Do a bulk first pass on
<module-test>(search-and-replace or scripted), or migrate by hand. - Fix compile errors (wrong generics on
assertThatThrownBy, broken nesting, etc.). - Run all audit
rgcommands above; fix every real violation. ./mvnw spotless:apply -pl <module>mvn -pl <module> test(ortest-compileif full test run is blocked).
A module is not migration-complete until the audit is clean (aside from documented exceptions) and tests pass.
When only exception tests are migrated in a file that otherwise stays on JUnit 4:
- Add AssertJ imports only for
assertThatThrownBy/assertThatCode - Leave existing
assertEquals/assertTrueunchanged unless explicitly migrating the whole file
When the whole file or module is being migrated, ignore the “leave unchanged” rule — apply all conventions in writing-tests.md and run the post-migration audit.
After changes, apply formatting, run the post-migration audit, and run tests for the affected module:
./mvnw spotless:apply -pl <module>
mvn -pl <module> testExample:
# bulk first pass on parquet-hadoop/src/test (search-and-replace or by hand)
# … run audit rg commands from Post-migration audit …
./mvnw spotless:apply -pl parquet-hadoop
mvn -pl parquet-hadoop testRun spotless:apply before committing so import order and formatting match the project style.