Assertions in Depth — assertEquals, assertTrue, assertThrows

9 min read

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 message

If 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

⚠️ Common mistakes

  • Comparing double without a delta. assertEquals(0.3, 0.1 + 0.2) fails because the result is 0.30000000000000004. Always use assertEquals(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 assertTimeout and expecting it to stop a slow call. If your goal is to fail fast in CI when an API call hangs, assertTimeout will not help — it waits for the call to finish. Use assertTimeoutPreemptively when you need a real deadline.

🎯 Practice task

Write a test class that exercises every assertion from this lesson. 25–35 minutes.

  1. Create an OrderService class with a method placeOrder(String item, int qty, double price) that:
    • Throws IllegalArgumentException("Quantity must be positive") if qty <= 0.
    • Throws IllegalArgumentException("Item cannot be blank") if item is blank.
    • Returns an Order object with total = qty * price.
  2. Write OrderServiceTest.java with:
    • An assertEquals on order.getTotal() with a delta (0.001).
    • An assertThrows for negative quantity — assert the exception message contains "positive".
    • An assertThrows for blank item name.
    • A assertTrue that order.getCreatedAt() is not null and is within the last second.
    • An assertDoesNotThrow for a valid edge case: qty = 1, price = 0.0.
  3. Test assertTimeoutPreemptively. Add a method to OrderService that has a 3-second Thread.sleep inside. Write a test with assertTimeoutPreemptively(Duration.ofSeconds(1), ...) and confirm the test fails in ~1 second, not 3.
  4. Stretch — list comparison. Add a getLineItems() method returning a List<String> like ["2x Widget — £4.99", "1x Gadget — £9.99"]. Assert the list with assertIterableEquals.

Next lesson: assertAll — how to run all assertions and see all failures at once.

// tip to track lessons you complete and pick up where you left off across devices.