This document explains the key design decisions behind Reality Check and the trade-offs involved.
Reality Check does not perform reflective object comparison. Libraries that recurse into your objects via setAccessible(true) introduce a class of subtle test failures:
$jacocoData) that pollute comparisonssetAccessible without --add-opens flagsStackOverflowErrorReality Check provides two stable alternatives:
Option A: Custom check (zero boilerplate)
public record PersonCheck(Person actual, FailureHandler failureHandler)
implements Check<PersonCheck, Person> {
@Override public PersonCheck self() { return this; }
public PersonCheck hasName(String name) {
return failureHandler.check(self(),
actual.getName().equals(name),
"expected name <%s> but was <%s>", name, actual.getName());
}
}
// Usage:
assertThat(person, PersonCheck::new).hasName("Yani");
Option B: Field extraction with existing checks
assertThat(person.getName()).isEqualTo("Yani");
assertThat(person.getAge()).isBetween(18, 65);
Both approaches produce clear, deterministic failure messages, work across all JVM configurations, and never touch your object’s internals via reflection.
Reality Check’s SoftFailureHandler is a simple CopyOnWriteArrayList<AssertionError> — no proxies, no bytecode generation, no @InjectSoftAssertions. This design:
CopyOnWriteArrayList is inherently safe for concurrent writes)ErrorCollector.intercept nesting problem that proxy-based approaches faceReality Check avoids Consumer/ThrowingConsumer overloads on the same method. assertThatThrownBy takes a dedicated ThrowingCallable type, so Kotlin’s SAM resolution is unambiguous. No special Kotlin module or extension functions are needed.
The realitycheck-core module has zero runtime dependencies. This avoids:
Format-specific modules (realitycheck-json, realitycheck-yaml) depend on their respective parsers (Jackson, SnakeYAML), but these are isolated — you only pull in what you use.
Using Java records as the extension mechanism (instead of abstract class inheritance) achieves:
self(), and implements CheckCheckFactory: The record constructor (ACTUAL, FailureHandler) matches the factory signature exactly, enabling assertThat(value, MyCheck::new)usingRecursiveComparison?See section 1 above. The trade-off is explicit: you write 3 lines of check code per type, and in return you get deterministic, reflection-free assertions that work across all JVM configurations.
Yes. See section 3 above. No special Kotlin module or workarounds are needed.
See section 2 above. Proxy-based soft assertions are more convenient to set up (@InjectSoftAssertions) but introduce complexity that surfaces as bugs in nesting, parallel execution, and non-Java JVM languages.
Reality Check uses sealed classes, records, and pattern matching — features that make the library’s API cleaner and its extension model simpler. Supporting Java 8 would require giving up the record-based Check interface that makes custom extensions 3 lines instead of 30.