Migrating tests (JUnit 4 → JUnit 5 + AssertJ)

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 mapping

JUnitAssertJ
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 runassertThatCode(() -> 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 exceptionassertThatThrownBy(…) 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).

Migrating custom assertThrows helpers

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:

  1. Replace with assertThatThrownBy + isInstanceOf.
  2. Also add hasMessage / hasMessageContaining using the actual message from the production throw statement or a test run — do not copy the old description string into hasMessage.
// 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(…)

Bulk first pass

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.

What a bulk rewrite can safely do

InputOutput
assertEquals(expected, actual)assertThat(actual).isEqualTo(expected)
assertArrayEquals(expected, actual)assertThat(actual).isEqualTo(expected)
assertNull / assertNotNullisNull / isNotNull
assertTrue / assertFalseisTrue / isFalse on the same boolean expression
assertSame / assertNotSameisSameAs / isNotSameAs
Assert.fail(…)AssertJ fail(…)
JUnit Assert / Hamcrest importsAssertJ static imports (when Assert was present)

What the bulk pass does not handle (requires a manual second pass)

Everything in writing-tests.md that is not in the table above — including but not limited to:

CategoryLeft behind by scriptTarget
ExceptionscatchThrowable + separate assertThat(caught)assertThatThrownBy + isInstanceOf + hasMessage*
Exceptions@Test(expected), JUnit assertThrows, TestUtils.assertThrows, try/catch/failassertThatThrownBy + isInstanceOf + hasMessage*
ExceptionsassertThatThrownBy with only isInstanceOfAdd hasMessage / hasMessageContaining from the throw site
ExceptionsOld assertThrows(description, …) description copied into hasMessageReal exception text, not the test description
CollectionsisEqualTo(Collections.emptyList()), isEqualTo(List.of(…)), isEqualTo(otherList)isEmpty(), containsExactly(…), containsExactlyElementsOf(…)
CollectionsassertThat(x.contains(…)).isTrue() / isEmpty().isTrue() / size().isEqualTo(n)contains, isEmpty, hasSize, hasSameSizeAs, …
OptionalassertThat(opt.isPresent()).isTrue() / isFalse()assertThat(opt).isPresent() / isEmpty()
NumericassertThat(comparison).isTrue() / isFalse()isZero, isPositive, isGreaterThan, hasSizeLessThan, …
NumericassertThat(x).isEqualTo(0) on integral resultsassertThat(x).isZero()
FilesassertThat(file.exists()).isTrue()assertThat(file).exists()
StringsassertThat(x.toString())assertThat(x).asString()
AssumptionsAssume.assumeTrue / Assumptions.assumeTrue / org.junit.jupiter.api.AssumptionsAssertJ assumeThat
Temp dirs@Rule TemporaryFolder, package-private @TempDir@TempDir private Path tempDir (+ accessor for subclasses)
Temp pathstoFile().getAbsolutePath(), new File(tempDir.toFile(), …), create+delete before writertempDir.resolve(…) + new Path(….toUri())
OrderingassertThat(a.compareTo(b) < 0).isTrue()assertThat(a).isLessThan(b) / isEqualByComparingTo
OrderingassertThat(compare(a, b)).isPositive() etc.Keep, but migrate from assertTrue(compare(…) > 0)
Type / referenceinstanceof / == / || wrapped in isTrue()isInstanceOf, isSameAs, isIn
IteratorsCustom drain helpers (assertIteratorEquals, …)assertThat(it).toIterable().containsExactly(…)
IteratorsassertThat(it.hasNext()).isFalse() / assertFalse(it.hasNext())assertThat(it).isExhausted()
StreamsassertThat(result).isEqualTo(stream.collect(…))containsExactlyElementsOf(…) after collecting
Maps / setshasSize + containsKey / contains on same subjectcontainsOnlyKeys / containsExactlyInAnyOrder
No exceptionassertTrue(true), empty try/catchassertThatCode(…).doesNotThrowAnyException()
Style.as("…") on assertThatThrownByOmit
StyleBlock lambda with one statement in assertThatThrownByExpression lambda or method reference
BugsassertThat(expr.method()).isEqualTo(…) — AssertJ chained on the valueassertThat(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.

Post-migration audit

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).

JUnit / Hamcrest leftovers

rg 'import static org\.junit\.Assert|import org\.junit\.Assert|org\.hamcrest|org\.junit\.Assume|Assumptions\.assumeTrue' <module-test>

Exceptions

# 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.

Collections and sizes

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>

Iterators and streams

rg 'assertThat\([^)]+\.hasNext\(\)\)\.(isTrue|isFalse)' <module-test>
rg 'assertFalse\([^)]+\.hasNext\(\)\)' <module-test>
rg 'assertThat\([^)]+\)\.isEqualTo\([^)]*\.stream\(\)' <module-test>
rg 'assertThat\([^)]+\.length\)\.isEqualTo' <module-test>

Numeric and boolean comparisons

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.

Files, strings, assumptions

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>

Ordering, type, and reference equality

rg 'compareTo\([^)]+\)\s*(<|>|==|!=)' <module-test>
rg 'assertThat\([^)]*instanceof[^)]*\)\.(isTrue|isFalse)' <module-test>
rg 'assertThat\([^)]*==[^)]*\)\.isTrue' <module-test>
rg 'assertThat\([^)]*\|\|[^)]*\)\.isTrue' <module-test>

Broken or flipped assertion chains

# 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>

No exception expected

rg 'assertTrue\(true\)|assertThat\(true\)\.isTrue' <module-test>
rg '\bassert\s+' <module-test>

Suggested workflow

  1. Do a bulk first pass on <module-test> (search-and-replace or scripted), or migrate by hand.
  2. Fix compile errors (wrong generics on assertThatThrownBy, broken nesting, etc.).
  3. Run all audit rg commands above; fix every real violation.
  4. ./mvnw spotless:apply -pl <module>
  5. mvn -pl <module> test (or test-compile if full test run is blocked).

A module is not migration-complete until the audit is clean (aside from documented exceptions) and tests pass.

Partial migration

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 / assertTrue unchanged 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.

Verification

After changes, apply formatting, run the post-migration audit, and run tests for the affected module:

./mvnw spotless:apply -pl <module>
mvn -pl <module> test

Example:

# 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 test

Run spotless:apply before committing so import order and formatting match the project style.