Compare assertion styles in Java tests: built-in JUnit assertions vs AssertJ vs Hamcrest. When would you choose each?
answer
- JUnit assertEquals = minimal, no deps
- Hamcrest: assertThat(actual, matcher) — composable, value-first
- AssertJ: assertThat(actual).chain() — fluent, IDE autocomplete
- AssertJ soft assertions collect all failures
- Hamcrest survives where frameworks (MockMvc) embed it
basics
~10 sJUnit's assertEquals is fine for simple checks. AssertJ gives fluent, chainable assertions like assertThat(list).contains(x) with great readability and IDE autocomplete. Hamcrest uses matcher objects like assertThat(x, is(5)). AssertJ is the modern default.
solid answer
~40 sAll three verify outcomes; they differ in ergonomics. JUnit Jupiter's built-in Assertions (assertEquals, assertTrue, assertThrows) are minimal and dependency-free — good for trivial checks. Hamcrest is a matcher library: you write assertThat(actual, matcher) where matchers like is(), hasItem(), containsString() compose; it reads naturally but the static-import sprawl and nesting can get awkward, and IDE discoverability is weak. AssertJ is the modern favorite: a single fluent entry point assertThat(actual) then chained, type-specific methods (.isEqualTo, .contains, .extracting, .hasSize) that autocomplete in the IDE, give rich failure messages, and support soft assertions and custom assertions. I default to AssertJ for readability and discoverability, keep JUnit's assertThrows for exceptions, and only use Hamcrest when a library (like Spring's MockMvc) already expects Hamcrest matchers.
code
java · 17 lines// JUnit built-in
assertEquals(3, names.size());
assertTrue(names.contains("Ada")); // weak message on failure
// Hamcrest (value-first matchers)
assertThat(names, hasSize(3));
assertThat(names, hasItem("Ada"));
assertThat(score, allOf(greaterThan(0), lessThan(100)));
// AssertJ (fluent, chainable)
assertThat(names)
.hasSize(3)
.contains("Ada")
.doesNotContain("Eve");
assertThatThrownBy(() -> service.load(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("id");go deeper
Can use JUnit's assertEquals/assertTrue and recognizes assertThat from AssertJ as a more readable alternative.
Explains the value-first (Hamcrest) vs fluent (AssertJ) difference, knows AssertJ chaining and assertThatThrownBy, and why messages differ.
Picks deliberately per context, uses soft/custom assertions, knows where Hamcrest is mandated (MockMvc), and standardizes one library per module.
Sets the org-wide assertion convention, weighs dependency/readability tradeoffs, and guides custom-assertion design for domain types to keep tests expressive.
## The job of an assertion After your test *acts* (calls the code under test), it must **verify** the result. An **assertion** is a statement that some condition holds; if it doesn't, the assertion throws an error and the test fails with a message explaining the discrepancy. The three families below all do this — they differ in *how you phrase the check* and *how good the failure message and IDE support are*. ## 1. JUnit built-in assertions These ship with JUnit Jupiter (`org.junit.jupiter.api.Assertions`), so no extra dependency: ```java assertEquals(5, result); // equality (expected, actual) assertTrue(list.isEmpty()); // a boolean condition assertNull(maybe); assertThrows(IOException.class, () -> read()); // exception thrown assertAll(() -> assertEquals(...), () -> assertTrue(...)); // group, report all failures ``` They're simple and adequate, but for anything beyond equality the messages are thin (`assertTrue(list.contains(x))` just says "expected true but was false" — it can't tell you what *was* in the list). ## 2. Hamcrest — the matcher approach **Hamcrest** introduced **matchers**: small reusable objects that describe an expectation. You write `assertThat(actual, matcher)`: ```java import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; assertThat(result, is(5)); assertThat(names, hasItem("Ada")); assertThat(text, containsString("error")); assertThat(value, allOf(greaterThan(0), lessThan(10))); // matchers compose ``` The big idea: matchers **compose** (`allOf`, `anyOf`, `not`) and produce a *self-describing* failure message ("Expected: a collection containing \"Ada\" but: was [...]"). Downsides: the assertion reads inside-out for complex cases, you need many static imports, and your IDE can't easily suggest which matcher applies because the value comes first — autocomplete can't narrow the matcher list by type. ## 3. AssertJ — the fluent approach **AssertJ** flips the ergonomics. You start every assertion with `assertThat(actual)` and then **chain** type-specific methods that your IDE autocompletes because it knows the type of `actual`: ```java import static org.assertj.core.api.Assertions.assertThat; assertThat(result).isEqualTo(5); assertThat(names).hasSize(3).contains("Ada").doesNotContain("Eve"); assertThat(text).startsWith("error").containsIgnoringCase("FILE"); assertThat(person).extracting(Person::getName).isEqualTo("Ada"); ``` Because `assertThat(names)` returns a *list assertion*, the IDE offers exactly the list-relevant methods (`contains`, `hasSize`, `containsExactly`…). Chaining multiple checks in one statement is natural. Failure messages are detailed (they print the actual collection). AssertJ also offers: - **Soft assertions** — collect *all* failures in a test instead of stopping at the first: `SoftAssertions soft = new SoftAssertions(); soft.assertThat(a)...; soft.assertAll();`. - **Exception assertions** — `assertThatThrownBy(() -> ...).isInstanceOf(IOException.class).hasMessageContaining("disk")`. - **Custom assertions** — extend `AbstractAssert` to make `assertThat(order).isShipped()` domain-specific. ## Choosing | Need | Pick | |---|---| | One trivial equality/null/throws check, zero extra deps | JUnit built-in | | Rich, readable, chainable assertions with IDE autocomplete (most code) | **AssertJ** (modern default) | | A library already speaks Hamcrest (Spring MockMvc `andExpect`, REST Assured) | Hamcrest, locally | Mixing two `assertThat` static imports in one file collides, so pick one fluent library per test class. The industry has largely standardized on **AssertJ** for general assertions because of discoverability and message quality, while Hamcrest survives mainly where frameworks embed it. ## Mapping to the discipline The universal idea is the **Assert** step of Arrange-Act-Assert. These libraries are competing Java *implementations* of that step; the choice is about failure-message quality and developer ergonomics, not about test correctness — a passing test passes under all three.
- What are AssertJ soft assertions and when are they useful?Soft assertions collect every assertion failure in a test and report them all at soft.assertAll(), instead of stopping at the first failure. Useful when verifying several independent properties of one result so you see the full picture in one run.
- Why does AssertJ get better IDE autocomplete than Hamcrest?Because assertThat(actual) returns a type-specific assertion object, so the IDE knows the type and can suggest only relevant methods. Hamcrest puts the value first and the matcher second, so the IDE can't narrow matcher suggestions by the value's type.
Hamcrest is like describing a person to a sketch artist with composable traits ('has glasses, not tall'); AssertJ is like handing the artist the photo and pointing — 'this nose, that smile' — chaining specifics with the subject already in hand.
saying these in an interview costs you the question
- Claiming AssertJ or Hamcrest changes whether a test passes — they only change ergonomics and messages
- Mixing AssertJ's and Hamcrest's assertThat static imports in the same class (name collision)
- Using assertTrue(collection.contains(x)) and getting an unhelpful 'expected true' message instead of a content-aware assertion
- Believing Hamcrest is dead — it's still required by Spring MockMvc and similar matcher-based APIs