Chapter 1 introduced the basic assertion toolkit. This lesson goes deeper: type-specific behaviour, the assertions you'll reach for when verifying REST API responses, the distinction between assertTimeout and assertTimeoutPreemptively, and the less-known but very useful assertLinesMatch. By the end you'll know not just which assertion to use but why — and why the wrong one produces confusing output.
assertEquals across different types
The single most-used assertion behaves differently depending on the types involved:
// Integers — exact match
assertEquals(200, response.getStatusCode());
// Strings — uses String.equals(), not reference equality
assertEquals("Alice", user.getName());
// Doubles — ALWAYS include a delta; floating-point arithmetic is inexact
assertEquals(89.99, product.getDiscountedPrice(10.0), 0.01);
// Lists — uses List.equals(), which checks size and element order
assertEquals(List.of("admin", "viewer"), user.getRoles());
// Custom objects — uses your class's equals() method
// If you don't override equals(), this compares references, not content
assertEquals(new Address("London", "EC1A"), user.getAddress());The last example is a trap. If Address doesn't override equals(), JUnit compares memory addresses and the assertion fails even when the content matches. The fix is to either override equals() on your data classes (from the Core Java for QA course, lesson on encapsulation) or use field-by-field assertions with assertAll.
assertThrows — verify exceptions and their messages
assertThrows returns the caught exception, which is the feature that makes it genuinely more useful than JUnit 4's @Test(expected = ...) or TestNG's @Test(expectedExceptions = ...):
// Basic: verify the exception type
assertThrows(IllegalArgumentException.class, () ->
userService.createUser("", "alice@test.com")
);
// Full: verify type AND message
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> userService.createUser("", "alice@test.com")
);
assertEquals("Name cannot be empty", ex.getMessage());
// Verify a cause
NullPointerException cause = assertThrows(
NullPointerException.class,
() -> userService.deleteUser(null)
);
assertNull(cause.getMessage()); // NPE with no messageIf the executable throws a different exception type, assertThrows re-throws it as-is — you'll see the unexpected exception in the test output, which tells you what actually happened.
assertTimeout vs assertTimeoutPreemptively
These look similar but behave very differently under the hood:
import java.time.Duration;
// assertTimeout: lets the code run to completion, then checks elapsed time
// Good when you can't safely interrupt the thread (e.g., cleanup code must run)
assertTimeout(Duration.ofSeconds(2), () -> {
apiClient.getProductList();
});
// assertTimeoutPreemptively: runs code in a separate thread, kills it at deadline
// Good for real CI time-boxing — the test actually stops after 2 seconds
assertTimeoutPreemptively(Duration.ofSeconds(2), () -> {
slowExternalService.fetch();
});Concretely: if slowExternalService.fetch() takes 10 seconds, assertTimeout waits the full 10 seconds and then reports the failure. assertTimeoutPreemptively kills the call after 2 seconds and reports immediately. Use assertTimeoutPreemptively in CI environments where you cannot afford a blocked build.
One caveat: assertTimeoutPreemptively runs the executable in a different thread from the test, which means thread-local state (Spring's @Transactional test support, for example) may not propagate. For plain unit tests and API tests this is not an issue.
assertIterableEquals — ordered collection comparison
When assertEquals on a List isn't available (for example, comparing a Set or a custom Iterable), use assertIterableEquals:
List<String> expected = List.of("Alice", "Bob", "Charlie");
List<String> actual = userService.getUserNames();
// assertEquals also works for List — assertIterableEquals works for any Iterable
assertIterableEquals(expected, actual);Both check element count and order. If you only care that the same elements are present regardless of order, sort both before asserting — JUnit 5 has no built-in assertContainsExactlyInAnyOrder. For that, add AssertJ (assertThat(actual).containsExactlyInAnyOrder("Alice", "Bob", "Charlie")), which pairs well with JUnit 5.
assertLinesMatch — text and log output with regex
assertLinesMatch compares two List<String> line by line with support for regex patterns and "fast-forward" markers:
List<String> expectedLines = List.of(
"User created: \\w+", // regex: any word
"Email: .+@.+\\..+", // regex: email pattern
">> 2", // fast-forward: skip next 2 lines
"Role assigned: admin"
);
List<String> actualLines = logCapture.getLines();
assertLinesMatch(expectedLines, actualLines);A line starting with >> followed by a number is a "fast-forward" marker — it tells JUnit to skip that many lines in the actual output. This is useful when you want to verify key lines in a log without having to match every timestamp or debug line. Most teams don't need assertLinesMatch day-to-day, but it is invaluable when testing log output or multi-line CLI responses.
The full assertion toolkit
JUnit 5 Assertions — Full Reference
| Method | Key detail | |
|---|---|---|
| Equality | assertEquals(expected, actual) / assertNotEquals | Use delta for doubles. Custom objects need equals() override. |
| Boolean | assertTrue(condition) / assertFalse(condition) | Prefer over assertEquals(true, value) — output is clearer. |
| Null checks | assertNull(value) / assertNotNull(value) | Fails with 'expected: not <null>' or 'expected: <null> but was: <X>'. |
| Exceptions | assertThrows(Type.class, () -> ...) / assertDoesNotThrow | assertThrows returns the exception — assert its message too. |
| Time limits | assertTimeout / assertTimeoutPreemptively | Preemptively aborts the thread. Timeout only checks after completion. |
| Collections & text | assertIterableEquals / assertLinesMatch | assertLinesMatch supports regex patterns and skip markers. |
⚠️ Common mistakes
- Comparing
doublewithout a delta.assertEquals(0.3, 0.1 + 0.2)fails because the result is0.30000000000000004. Always useassertEquals(0.3, 0.1 + 0.2, 1e-9). If you see a floating-point assertion fail with a tiny discrepancy, this is why. - Not asserting the exception message after
assertThrows. Verifying the type alone (IllegalArgumentException) is often not enough — many exception types are thrown for different reasons. Always assert the message or a property of the exception when the message matters for correctness. - Using
assertTimeoutand expecting it to stop a slow call. If your goal is to fail fast in CI when an API call hangs,assertTimeoutwill not help — it waits for the call to finish. UseassertTimeoutPreemptivelywhen you need a real deadline.
🎯 Practice task
Write a test class that exercises every assertion from this lesson. 25–35 minutes.
- Create an
OrderServiceclass with a methodplaceOrder(String item, int qty, double price)that:- Throws
IllegalArgumentException("Quantity must be positive")ifqty <= 0. - Throws
IllegalArgumentException("Item cannot be blank")ifitemis blank. - Returns an
Orderobject withtotal = qty * price.
- Throws
- Write
OrderServiceTest.javawith:- An
assertEqualsonorder.getTotal()with a delta (0.001). - An
assertThrowsfor negative quantity — assert the exception message contains "positive". - An
assertThrowsfor blank item name. - A
assertTruethatorder.getCreatedAt()is not null and is within the last second. - An
assertDoesNotThrowfor a valid edge case:qty = 1,price = 0.0.
- An
- Test
assertTimeoutPreemptively. Add a method toOrderServicethat has a 3-secondThread.sleepinside. Write a test withassertTimeoutPreemptively(Duration.ofSeconds(1), ...)and confirm the test fails in ~1 second, not 3. - Stretch — list comparison. Add a
getLineItems()method returning aList<String>like["2x Widget — £4.99", "1x Gadget — £9.99"]. Assert the list withassertIterableEquals.
Next lesson: assertAll — how to run all assertions and see all failures at once.