Appearance
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:
| Letter | Principle | Means |
|---|---|---|
| F | Fast | milliseconds — so you run them constantly |
| I | Isolated / Independent | no shared state; any order; one reason to fail |
| R | Repeatable | same result every run, every machine |
| S | Self-validating | a clear pass/fail, no manual log-reading |
| T | Timely | written 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 — alwaysThe 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 updatesA 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; useusingRecursiveComparisonfor field-by-field checks. Don't leave a terminal-lessassertThat. - 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.sleepto await async work — wait on a condition (latch/Future/polling). - Determinism underpins safe parallelism and trustworthy CI.
Lesson Quiz
How do you make code that reads the current date testable and deterministic?
What does the 'R' in FIRST stand for?
Why avoid Thread.sleep(500) to wait for async work in a test?
A benefit of AssertJ over plain assertEquals/assertTrue?
To make randomized logic reproducible in a test, you should…
Next: Module 15 · Test Architecture & Frameworks. This module's lab is in labs/src/test/java/com/jse21/m14_testing/.