Skip to content

Lesson 04 · Readable Assertions & Deterministic Tests

Beyond the 1Z0-830 exam

A test that passes today and fails tomorrow with no code change ("flaky") is worse than no test — it erodes trust in the whole suite. This lesson is about assertions that read well and tests that are deterministic: same inputs, same result, every run.

Objectives

After this lesson you will be able to:

  • Write fluent, descriptive assertions with AssertJ.
  • Apply the FIRST principles.
  • Make time- and randomness-dependent code deterministic (inject Clock, seed RNGs).
  • Test concurrent code without sleeps.

AssertJ — fluent, discoverable assertions

AssertJ chains from a single assertThat, giving readable code and rich failure messages:

java
import static org.assertj.core.api.Assertions.*;

assertThat(quote.convertedCents()).isEqualTo(900);
assertThat(names).containsExactly("Ada", "Linus").doesNotContain("X");
assertThat(user.email()).startsWith("ada@").endsWith(".org");
assertThat(result).isNotNull().extracting(Quote::currency).isEqualTo("EUR");
assertThatThrownBy(() -> svc.quote(-1, "EUR"))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessageContaining("amount");

Versus JUnit's assertEquals, AssertJ reads left-to-right ("assert that names contains exactly…") and its IDE autocomplete discovers the right assertion for the type. One assertThat per subject, chained — not a pile of assertTrue calls that all report "expected true but was false."

Gotcha

assertThat(x).isEqualTo(y) uses equals. To compare object contents field-by-field (ignoring equals), use usingRecursiveComparison().isEqualTo(expected). And don't leave a dangling assertThat(x); with no terminal call — it asserts nothing and silently passes.

FIRST principles

Good unit tests are:

LetterPrincipleMeans
FFastmilliseconds — so you run them constantly
IIsolated / Independentno shared state; any order; one reason to fail
RRepeatablesame result every run, every machine
SSelf-validatinga clear pass/fail, no manual log-reading
TTimelywritten with (ideally before) the code

"Repeatable" and "Isolated" are where flakiness comes from — and both are fixable by design.

Determinism: control time and randomness

Code that reads Instant.now() or Math.random() directly is untestable — its output changes every run. Inject the source instead:

java
// SUT depends on a Clock, not the wall clock:
public PricingService(RateProvider rates, Clock clock) { ... }
LocalDate.now(clock);                      // reads the injected clock

// Test pins it:
Clock fixed = Clock.fixed(Instant.parse("2026-06-19T10:00:00Z"), ZoneOffset.UTC);
assertThat(new PricingService(rates, fixed).isWeekend()).isFalse(); // Friday — always

The same move works for randomness: pass a seeded new Random(42) (or a RandomGenerator) so the "random" sequence is reproducible. Anything nondeterministic — time, randomness, locale, default time zone, environment — should be an injectable input, not a hidden global read.

Trap — Thread.sleep in tests

Never Thread.sleep to "wait for" async work — too short flakes, too long wastes time. Wait on a condition: a CountDownLatch/Future.get(timeout) for completion, or a polling assertion (Awaitility's await().untilAsserted(...)). Assert on the outcome, not the clock.

Testing concurrent code

Deterministically exercise concurrency without sleeps:

java
var pool = Executors.newFixedThreadPool(8);
var latch = new CountDownLatch(1000);
var counter = new AtomicInteger();
for (int i = 0; i < 1000; i++) {
    pool.submit(() -> { counter.incrementAndGet(); latch.countDown(); });
}
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();  // bounded wait on a condition
assertThat(counter.get()).isEqualTo(1000);              // no lost updates

A latch.await(timeout) blocks until the work signals done (or fails fast on timeout) — repeatable, unlike a fixed sleep.

SDET note

Determinism is the foundation of everything later in Part B: parallel test execution (Module 15) is only safe when tests are isolated, and CI (Module 20) only stays trustworthy when a red build means a real bug — not a coin flip. Quarantine a flaky test; don't @Disabled-and-forget it.

Key Takeaways

  • AssertJ: one assertThat(subject) chained into readable, type-aware assertions with better failure messages; use usingRecursiveComparison for field-by-field checks. Don't leave a terminal-less assertThat.
  • FIRST: Fast, Isolated, Repeatable, Self-validating, Timely — flakiness is a violation of Isolated/Repeatable.
  • Make tests deterministic by injecting time (Clock.fixed) and seeding randomness; never read hidden globals (now, random, locale, time zone).
  • Never Thread.sleep to await async work — wait on a condition (latch/Future/polling).
  • Determinism underpins safe parallelism and trustworthy CI.

Lesson Quiz

Lesson Quiz · Readable Assertions & Deterministic Tests0 / 5
  1. How do you make code that reads the current date testable and deterministic?

    • ARun tests only at midnight
    • BInject a java.time.Clock and use LocalDate.now(clock); pin it with Clock.fixed in tests
    • CMock LocalDate statically every time
    • DAdd Thread.sleep to stabilize it
  2. What does the 'R' in FIRST stand for?

    • AReadable
    • BRepeatable
    • CRefactorable
    • DRandom
  3. Why avoid Thread.sleep(500) to wait for async work in a test?

    • AIt's deprecated
    • BIt's flaky and slow — too short fails intermittently, too long wastes time; wait on a condition instead
    • CIt doesn't compile
    • DSleep isn't allowed in JUnit
  4. A benefit of AssertJ over plain assertEquals/assertTrue?

    • AIt runs tests in parallel
    • BFluent, chainable, type-aware assertions with clearer failure messages
    • CIt mocks dependencies
    • DIt's required by JUnit 5
  5. To make randomized logic reproducible in a test, you should…

    • ARun it many times and average
    • BInject a seeded Random (e.g. new Random(42)) so the sequence repeats
    • CUse Math.random() directly
    • DDisable the test

Next: Module 15 · Test Architecture & Frameworks. This module's lab is in labs/src/test/java/com/jse21/m14_testing/.