Repository files navigation

dokimi-assert

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run.

CILicenceJava

dependencies {
testImplementation("dev.dokimi:assert-core:0.1.0")
testImplementation("dev.dokimi:assert-kotlin:0.1.0") // coroutines
}

Java 17 and up. No runtime dependencies beyond JSpecify's annotations.

Getting started

classStoreTest {
@RegisterExtensionfinalSeatExtensionseat = newSeatExtension();
@Testvoidget() {
varitem = store.get("widget");
Check.isNotNull(seat, item, "get answers the stored item");
Check.equal(seat, item.name(), "widget", "and the item is the one stored");
}
}

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionFailed: and the item is the one stored: want "widget", got "gadget"

What a seat is

The seat is where a failure goes. Assertions never call a test framework and never throw on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

SeatCheck doesSoft does
Collector, from SeatExtensionthrowscollects, thrown when the test ends
Standardthrowsthrows
Recordercollectscollects

SeatExtension is a Seat itself, so it goes straight into a call. It is a field rather than a parameter, because a parameter resolver hands out a value JUnit then forgets: nothing would be left holding the collector when the body ends. It is the only class here that mentions JUnit, and it is optional.

Two surfaces

Check stops at the first failure. Soft records and carries on, so one run shows every property that failed.

Check.equal(seat, reply.status(), 200, "the request succeeds");
Soft.hasPrefix(seat, reply.body(), "{", "the body is JSON");
Soft.length(seat, reply.items(), 3, "every item comes back");

If both Soft calls fail, both are reported together:

AssertionFailed: 2 failures:
1. the body is JSON: "[1,2]" does not start with "{"
2. every item comes back: expected length 3, got 2

Several assertions about one value

that starts a chain, so a value is named once and every method after it answers the chain:

Check.that(seat, reply.status())
.notEqual(0, "the status was set")
.equal(200, "the request succeeds");

The chain fixes the value's type, so want is held to it and a mismatch is a compile error:

Check.that(seat, reply.body()).equal(200, "..."); // does not compileCheck.equal(seat, reply.body(), 200, "..."); // compiles, fails at run time

The static form cannot be tightened the same way. Java infers a common supertype for two arguments of a generic method, so equal(seat, "1", 1, ...) would still compile whatever the parameters were declared as.

A chain from Check stops at the first failing method. One from Soft runs them all and reports each failure.

The assertions

Thirty-four on Check and thirty-three on Soft, since only Check can drive an assertion to failure. Three more compare against a golden file, and the benchmark contract states three ceilings. that starts a chain over any of the value assertions.

Every assertion takes the seat first and the message last. Check and Soft carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

Check.equal(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)
Check.notEqual(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)

Truth and absence — Java has one null, so this is simpler than the JavaScript column.

Check.isTrue(Seatseat, booleancondition, Stringmsg)
Check.isFalse(Seatseat, booleancondition, Stringmsg)
Check.isNull(Seatseat, Objectgot, Stringmsg)
Check.isNotNull(Seatseat, Objectgot, Stringmsg)

Size — A CharSequence, a Collection, a Map or an array.

Check.length(Seatseat, Objectgot, intwant, Stringmsg)
Check.isEmpty(Seatseat, Objectgot, Stringmsg)
Check.isNotEmpty(Seatseat, Objectgot, Stringmsg)

Containment — What holding means follows the haystack.

Check.contains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.notContains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.containsInOrder(Seatseat, Objectgot, String[] needles, Stringmsg)

Text — CharSequence.

Check.hasPrefix(Seatseat, Objectgot, Stringprefix, Stringmsg)
Check.hasSuffix(Seatseat, Objectgot, Stringsuffix, Stringmsg)
Check.matches(Seatseat, Objectgot, Stringpattern, Stringmsg)

Numbers — Where exact equality is the wrong question.

Check.closeTo(Seatseat, Objectgot, doublewant, doubletolerance, Stringmsg)
Check.inRange(Seatseat, Objectgot, doublelow, doublehigh, Stringmsg)

Errors — For code that hands an exception back. Matching follows the cause chain.

Check.noError(Seatseat, Throwableerror, Stringmsg)
Check.hasError(Seatseat, Throwableerror, Stringmsg)
Check.errorIs(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorIsNot(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorAs(Seatseat, Throwableerror, Class<E> want, Stringmsg) -> @NullableE

ThrowingthrowsException, because throws is a keyword.

Check.throwsException(Seatseat, Raises.Bodybody, Stringmsg) -> @NullableThrowableCheck.doesNotThrow(Seatseat, Raises.Bodybody, Stringmsg)

Ordering — Sorted, unique, and anything else that holds between neighbours.

Check.pairwise(Seatseat, List<T> items, BiPredicate<T, T> predicate, Stringmsg)

Cancellation — Interruption is Java's cancellation: what sleep, wait and take respond to.

Check.honoursCancellation(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.honoursDeadline(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.completesWithin(Seatseat, Durationwithin, Raises.Bodybody, Stringmsg)

Purity and a missing handle — What observe answers defines what nothing means.

Check.isPure(Seatseat, Callable<Object> observe, Raises.Bodybody, Stringmsg, Option... options)
Check.nullHandleSafe(Seatseat, Behaviour.Handledbody, Stringmsg)

Retrying — For a condition something outside the test makes true. Both spend real time.

Check.eventually(Seatseat, Durationtimeout, Durationinterval, Consumer<Seat> body, Stringmsg)
Check.eventuallyTrue(Seatseat, Durationtimeout, BooleanSupplierpredicate, Stringmsg)

Concurrency — Reads the live non-daemon threads either side of the scope.

Check.noTaskLeaks(Seatseat, Stringmsg) -> Runnable

Testing an assertion — On Check only: Soft cannot drive a check to failure.

Check.rejects(Seatseat, Stringmsg, Consumer<Recorder> body) -> String

Golden files — recorded output, compared and rewritable.

Golden.shouldUpdate() -> booleanGolden.scrubTimestamps() -> ScrubberGolden.scrubHashes() -> ScrubberGolden.scrubRunIds() -> ScrubberGolden.scrubJsonFields(String... fields) -> ScrubberGolden.matchAt(Seatseat, Pathpath, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.match(Seatseat, Stringname, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.matchJsonField(Seatseat, Pathpath, Stringfield, Stringgot, booleanupdate, Scrubber... scrubbers)

Benchmark ceilings — chained onto one contract.

newContract(Seatseat, Stringmsg)
Contract.maxLatency(Durationceiling) -> ContractContract.maxMean(Durationceiling) -> ContractContract.maxBytes(longceiling) -> ContractContract.loop(intiterations, Raises.Bodybody) -> ContractContract.check() -> void

Coroutines — from dokimi-assert-kotlin, for the six a Java signature cannot reach.

Check.honoursCancellation(seat:Seat, msg:String, body:suspend () ->Unit)
Check.honoursDeadline(seat:Seat, msg:String, body:suspend () ->Unit)
Check.completesWithin(seat:Seat, within:Duration, msg:String, body:suspend () ->Unit)
Check.eventually(seat:Seat, timeout:Duration, interval:Duration, msg:String, body:suspend (Seat) ->Unit)
Check.eventuallyTrue(seat:Seat, timeout:Duration, msg:String, predicate:suspend () ->Boolean)
Check.noTaskLeaks(seat:Seat, msg:String, body:suspend (CoroutineScope) ->Unit)

Each one carries a doc comment: what it states, what every argument means, the edge cases it decides, and a worked call.

Equality

Object.equals answers a different question in three places, and this corrects all three:

ExpressionequalsHere
Double.valueOf(NaN).equals(NaN)truenot equal, per IEEE 754
Double.valueOf(0.0).equals(-0.0)falseequal
new int[]{1}.equals(new int[]{1})falseequal
Integer.valueOf(1).equals(1L)falsenot equal, and for the right reason

Comparison is structural and reaches arrays, collections and maps, including an array nested inside a list that otherwise compares by value. Different classes never compare, and a cycle stops the walk.

Pass Option.EQUATE_NANS or Option.EQUATE_EMPTY to relax either for one call. An option applies to the call it is passed to and nothing else.

Kotlin

Kotlin calls the Java artifact for thirty-five of the forty-one. The other six take work that suspends, and a Kotlin suspend lambda compiles to a method taking a hidden Continuation, so no Java method can accept one. dokimi-assert-kotlin supplies those:

classWorkerTest {
@Test
fun`it stops when told`() = runTest {
Check.honoursCancellation(seat, "the worker stops when told") {
worker.serve()
}
}
}

Cancellation there is a coroutine's own: a Job cancelled and a CancellationException raised at the next suspension point. In the Java artifact it is Thread.interrupt, which is what sleep, wait, take and every blocking call in java.util.concurrent respond to.

The standard

The assertions are defined in assert-spec, language-neutral and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records what this language cannot supply, and what a check cannot see.

Where Java differs

bench.Contract.maxAllocs is declared rather than implemented. The JVM reports bytes allocated per thread and no count of allocations; JFR samples allocation events rather than counting them. maxBytes is implemented, and holds a ceiling on the same behaviour by weight: ThreadMXBean.getThreadAllocatedBytes counts what a thread allocated rather than what survived a collection, so the reading does not move with the collector.

noTaskLeaks sees platform threads and executor threads. A leaked virtual thread is not reported, because virtual threads appear in no standard enumeration on any JVM version. The overlay records that as a limit rather than leaving it to be discovered.

throwsException, because throws is a keyword. It takes a Body rather than a Runnable, so a caller need not wrap a method that declares a checked exception.

Development

./gradlew build # compile, test, document, jar
./gradlew test
./gradlew javadoc
./gradlew centralBundle # both artifacts as one Maven Central bundle

Building needs JDK 23 or newer, because the doc comments are Markdown and older javadoc reads /// as an ordinary comment. The artifact targets Java 17 regardless, and CI runs the tests on a real 17.

Pushing a v* tag builds a signed bundle and uploads it to the Central Portal, which validates it and waits for someone to release it. The build signs only when SIGNING_KEY and SIGNING_PASSWORD are in the environment, so an ordinary build needs no key.

docs/rfc/0001 records what Java does differently from the other implementations of this standard, and why.

Licence

MIT. See LICENSE.

About

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run.

CILicenceJava

dependencies {
testImplementation("dev.dokimi:assert-core:0.1.0")
testImplementation("dev.dokimi:assert-kotlin:0.1.0") // coroutines
}

Java 17 and up. No runtime dependencies beyond JSpecify's annotations.

Getting started

classStoreTest {
@RegisterExtensionfinalSeatExtensionseat = newSeatExtension();
@Testvoidget() {
varitem = store.get("widget");
Check.isNotNull(seat, item, "get answers the stored item");
Check.equal(seat, item.name(), "widget", "and the item is the one stored");
}
}

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionFailed: and the item is the one stored: want "widget", got "gadget"

What a seat is

The seat is where a failure goes. Assertions never call a test framework and never throw on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

SeatCheck doesSoft does
Collector, from SeatExtensionthrowscollects, thrown when the test ends
Standardthrowsthrows
Recordercollectscollects

SeatExtension is a Seat itself, so it goes straight into a call. It is a field rather than a parameter, because a parameter resolver hands out a value JUnit then forgets: nothing would be left holding the collector when the body ends. It is the only class here that mentions JUnit, and it is optional.

Two surfaces

Check stops at the first failure. Soft records and carries on, so one run shows every property that failed.

Check.equal(seat, reply.status(), 200, "the request succeeds");
Soft.hasPrefix(seat, reply.body(), "{", "the body is JSON");
Soft.length(seat, reply.items(), 3, "every item comes back");

If both Soft calls fail, both are reported together:

AssertionFailed: 2 failures:
1. the body is JSON: "[1,2]" does not start with "{"
2. every item comes back: expected length 3, got 2

Several assertions about one value

that starts a chain, so a value is named once and every method after it answers the chain:

Check.that(seat, reply.status())
.notEqual(0, "the status was set")
.equal(200, "the request succeeds");

The chain fixes the value's type, so want is held to it and a mismatch is a compile error:

Check.that(seat, reply.body()).equal(200, "..."); // does not compileCheck.equal(seat, reply.body(), 200, "..."); // compiles, fails at run time

The static form cannot be tightened the same way. Java infers a common supertype for two arguments of a generic method, so equal(seat, "1", 1, ...) would still compile whatever the parameters were declared as.

A chain from Check stops at the first failing method. One from Soft runs them all and reports each failure.

The assertions

Thirty-four on Check and thirty-three on Soft, since only Check can drive an assertion to failure. Three more compare against a golden file, and the benchmark contract states three ceilings. that starts a chain over any of the value assertions.

Every assertion takes the seat first and the message last. Check and Soft carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

Check.equal(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)
Check.notEqual(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)

Truth and absence — Java has one null, so this is simpler than the JavaScript column.

Check.isTrue(Seatseat, booleancondition, Stringmsg)
Check.isFalse(Seatseat, booleancondition, Stringmsg)
Check.isNull(Seatseat, Objectgot, Stringmsg)
Check.isNotNull(Seatseat, Objectgot, Stringmsg)

Size — A CharSequence, a Collection, a Map or an array.

Check.length(Seatseat, Objectgot, intwant, Stringmsg)
Check.isEmpty(Seatseat, Objectgot, Stringmsg)
Check.isNotEmpty(Seatseat, Objectgot, Stringmsg)

Containment — What holding means follows the haystack.

Check.contains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.notContains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.containsInOrder(Seatseat, Objectgot, String[] needles, Stringmsg)

Text — CharSequence.

Check.hasPrefix(Seatseat, Objectgot, Stringprefix, Stringmsg)
Check.hasSuffix(Seatseat, Objectgot, Stringsuffix, Stringmsg)
Check.matches(Seatseat, Objectgot, Stringpattern, Stringmsg)

Numbers — Where exact equality is the wrong question.

Check.closeTo(Seatseat, Objectgot, doublewant, doubletolerance, Stringmsg)
Check.inRange(Seatseat, Objectgot, doublelow, doublehigh, Stringmsg)

Errors — For code that hands an exception back. Matching follows the cause chain.

Check.noError(Seatseat, Throwableerror, Stringmsg)
Check.hasError(Seatseat, Throwableerror, Stringmsg)
Check.errorIs(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorIsNot(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorAs(Seatseat, Throwableerror, Class<E> want, Stringmsg) -> @NullableE

ThrowingthrowsException, because throws is a keyword.

Check.throwsException(Seatseat, Raises.Bodybody, Stringmsg) -> @NullableThrowableCheck.doesNotThrow(Seatseat, Raises.Bodybody, Stringmsg)

Ordering — Sorted, unique, and anything else that holds between neighbours.

Check.pairwise(Seatseat, List<T> items, BiPredicate<T, T> predicate, Stringmsg)

Cancellation — Interruption is Java's cancellation: what sleep, wait and take respond to.

Check.honoursCancellation(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.honoursDeadline(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.completesWithin(Seatseat, Durationwithin, Raises.Bodybody, Stringmsg)

Purity and a missing handle — What observe answers defines what nothing means.

Check.isPure(Seatseat, Callable<Object> observe, Raises.Bodybody, Stringmsg, Option... options)
Check.nullHandleSafe(Seatseat, Behaviour.Handledbody, Stringmsg)

Retrying — For a condition something outside the test makes true. Both spend real time.

Check.eventually(Seatseat, Durationtimeout, Durationinterval, Consumer<Seat> body, Stringmsg)
Check.eventuallyTrue(Seatseat, Durationtimeout, BooleanSupplierpredicate, Stringmsg)

Concurrency — Reads the live non-daemon threads either side of the scope.

Check.noTaskLeaks(Seatseat, Stringmsg) -> Runnable

Testing an assertion — On Check only: Soft cannot drive a check to failure.

Check.rejects(Seatseat, Stringmsg, Consumer<Recorder> body) -> String

Golden files — recorded output, compared and rewritable.

Golden.shouldUpdate() -> booleanGolden.scrubTimestamps() -> ScrubberGolden.scrubHashes() -> ScrubberGolden.scrubRunIds() -> ScrubberGolden.scrubJsonFields(String... fields) -> ScrubberGolden.matchAt(Seatseat, Pathpath, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.match(Seatseat, Stringname, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.matchJsonField(Seatseat, Pathpath, Stringfield, Stringgot, booleanupdate, Scrubber... scrubbers)

Benchmark ceilings — chained onto one contract.

newContract(Seatseat, Stringmsg)
Contract.maxLatency(Durationceiling) -> ContractContract.maxMean(Durationceiling) -> ContractContract.maxBytes(longceiling) -> ContractContract.loop(intiterations, Raises.Bodybody) -> ContractContract.check() -> void

Coroutines — from dokimi-assert-kotlin, for the six a Java signature cannot reach.

Check.honoursCancellation(seat:Seat, msg:String, body:suspend () ->Unit)
Check.honoursDeadline(seat:Seat, msg:String, body:suspend () ->Unit)
Check.completesWithin(seat:Seat, within:Duration, msg:String, body:suspend () ->Unit)
Check.eventually(seat:Seat, timeout:Duration, interval:Duration, msg:String, body:suspend (Seat) ->Unit)
Check.eventuallyTrue(seat:Seat, timeout:Duration, msg:String, predicate:suspend () ->Boolean)
Check.noTaskLeaks(seat:Seat, msg:String, body:suspend (CoroutineScope) ->Unit)

Each one carries a doc comment: what it states, what every argument means, the edge cases it decides, and a worked call.

Equality

Object.equals answers a different question in three places, and this corrects all three:

ExpressionequalsHere
Double.valueOf(NaN).equals(NaN)truenot equal, per IEEE 754
Double.valueOf(0.0).equals(-0.0)falseequal
new int[]{1}.equals(new int[]{1})falseequal
Integer.valueOf(1).equals(1L)falsenot equal, and for the right reason

Comparison is structural and reaches arrays, collections and maps, including an array nested inside a list that otherwise compares by value. Different classes never compare, and a cycle stops the walk.

Pass Option.EQUATE_NANS or Option.EQUATE_EMPTY to relax either for one call. An option applies to the call it is passed to and nothing else.

Kotlin

Kotlin calls the Java artifact for thirty-five of the forty-one. The other six take work that suspends, and a Kotlin suspend lambda compiles to a method taking a hidden Continuation, so no Java method can accept one. dokimi-assert-kotlin supplies those:

classWorkerTest {
@Test
fun`it stops when told`() = runTest {
Check.honoursCancellation(seat, "the worker stops when told") {
worker.serve()
}
}
}

Cancellation there is a coroutine's own: a Job cancelled and a CancellationException raised at the next suspension point. In the Java artifact it is Thread.interrupt, which is what sleep, wait, take and every blocking call in java.util.concurrent respond to.

The standard

The assertions are defined in assert-spec, language-neutral and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records what this language cannot supply, and what a check cannot see.

Where Java differs

bench.Contract.maxAllocs is declared rather than implemented. The JVM reports bytes allocated per thread and no count of allocations; JFR samples allocation events rather than counting them. maxBytes is implemented, and holds a ceiling on the same behaviour by weight: ThreadMXBean.getThreadAllocatedBytes counts what a thread allocated rather than what survived a collection, so the reading does not move with the collector.

noTaskLeaks sees platform threads and executor threads. A leaked virtual thread is not reported, because virtual threads appear in no standard enumeration on any JVM version. The overlay records that as a limit rather than leaving it to be discovered.

throwsException, because throws is a keyword. It takes a Body rather than a Runnable, so a caller need not wrap a method that declares a checked exception.

Development

./gradlew build # compile, test, document, jar
./gradlew test
./gradlew javadoc
./gradlew centralBundle # both artifacts as one Maven Central bundle

Building needs JDK 23 or newer, because the doc comments are Markdown and older javadoc reads /// as an ordinary comment. The artifact targets Java 17 regardless, and CI runs the tests on a real 17.

Pushing a v* tag builds a signed bundle and uploads it to the Central Portal, which validates it and waits for someone to release it. The build signs only when SIGNING_KEY and SIGNING_PASSWORD are in the environment, so an ordinary build needs no key.

docs/rfc/0001 records what Java does differently from the other implementations of this standard, and why.

Licence

MIT. See LICENSE.

About

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run.

CILicenceJava

dependencies {
testImplementation("dev.dokimi:assert-core:0.1.0")
testImplementation("dev.dokimi:assert-kotlin:0.1.0") // coroutines
}

Java 17 and up. No runtime dependencies beyond JSpecify's annotations.

Getting started

classStoreTest {
@RegisterExtensionfinalSeatExtensionseat = newSeatExtension();
@Testvoidget() {
varitem = store.get("widget");
Check.isNotNull(seat, item, "get answers the stored item");
Check.equal(seat, item.name(), "widget", "and the item is the one stored");
}
}

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionFailed: and the item is the one stored: want "widget", got "gadget"

What a seat is

The seat is where a failure goes. Assertions never call a test framework and never throw on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

SeatCheck doesSoft does
Collector, from SeatExtensionthrowscollects, thrown when the test ends
Standardthrowsthrows
Recordercollectscollects

SeatExtension is a Seat itself, so it goes straight into a call. It is a field rather than a parameter, because a parameter resolver hands out a value JUnit then forgets: nothing would be left holding the collector when the body ends. It is the only class here that mentions JUnit, and it is optional.

Two surfaces

Check stops at the first failure. Soft records and carries on, so one run shows every property that failed.

Check.equal(seat, reply.status(), 200, "the request succeeds");
Soft.hasPrefix(seat, reply.body(), "{", "the body is JSON");
Soft.length(seat, reply.items(), 3, "every item comes back");

If both Soft calls fail, both are reported together:

AssertionFailed: 2 failures:
1. the body is JSON: "[1,2]" does not start with "{"
2. every item comes back: expected length 3, got 2

Several assertions about one value

that starts a chain, so a value is named once and every method after it answers the chain:

Check.that(seat, reply.status())
.notEqual(0, "the status was set")
.equal(200, "the request succeeds");

The chain fixes the value's type, so want is held to it and a mismatch is a compile error:

Check.that(seat, reply.body()).equal(200, "..."); // does not compileCheck.equal(seat, reply.body(), 200, "..."); // compiles, fails at run time

The static form cannot be tightened the same way. Java infers a common supertype for two arguments of a generic method, so equal(seat, "1", 1, ...) would still compile whatever the parameters were declared as.

A chain from Check stops at the first failing method. One from Soft runs them all and reports each failure.

The assertions

Thirty-four on Check and thirty-three on Soft, since only Check can drive an assertion to failure. Three more compare against a golden file, and the benchmark contract states three ceilings. that starts a chain over any of the value assertions.

Every assertion takes the seat first and the message last. Check and Soft carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

Check.equal(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)
Check.notEqual(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)

Truth and absence — Java has one null, so this is simpler than the JavaScript column.

Check.isTrue(Seatseat, booleancondition, Stringmsg)
Check.isFalse(Seatseat, booleancondition, Stringmsg)
Check.isNull(Seatseat, Objectgot, Stringmsg)
Check.isNotNull(Seatseat, Objectgot, Stringmsg)

Size — A CharSequence, a Collection, a Map or an array.

Check.length(Seatseat, Objectgot, intwant, Stringmsg)
Check.isEmpty(Seatseat, Objectgot, Stringmsg)
Check.isNotEmpty(Seatseat, Objectgot, Stringmsg)

Containment — What holding means follows the haystack.

Check.contains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.notContains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.containsInOrder(Seatseat, Objectgot, String[] needles, Stringmsg)

Text — CharSequence.

Check.hasPrefix(Seatseat, Objectgot, Stringprefix, Stringmsg)
Check.hasSuffix(Seatseat, Objectgot, Stringsuffix, Stringmsg)
Check.matches(Seatseat, Objectgot, Stringpattern, Stringmsg)

Numbers — Where exact equality is the wrong question.

Check.closeTo(Seatseat, Objectgot, doublewant, doubletolerance, Stringmsg)
Check.inRange(Seatseat, Objectgot, doublelow, doublehigh, Stringmsg)

Errors — For code that hands an exception back. Matching follows the cause chain.

Check.noError(Seatseat, Throwableerror, Stringmsg)
Check.hasError(Seatseat, Throwableerror, Stringmsg)
Check.errorIs(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorIsNot(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorAs(Seatseat, Throwableerror, Class<E> want, Stringmsg) -> @NullableE

ThrowingthrowsException, because throws is a keyword.

Check.throwsException(Seatseat, Raises.Bodybody, Stringmsg) -> @NullableThrowableCheck.doesNotThrow(Seatseat, Raises.Bodybody, Stringmsg)

Ordering — Sorted, unique, and anything else that holds between neighbours.

Check.pairwise(Seatseat, List<T> items, BiPredicate<T, T> predicate, Stringmsg)

Cancellation — Interruption is Java's cancellation: what sleep, wait and take respond to.

Check.honoursCancellation(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.honoursDeadline(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.completesWithin(Seatseat, Durationwithin, Raises.Bodybody, Stringmsg)

Purity and a missing handle — What observe answers defines what nothing means.

Check.isPure(Seatseat, Callable<Object> observe, Raises.Bodybody, Stringmsg, Option... options)
Check.nullHandleSafe(Seatseat, Behaviour.Handledbody, Stringmsg)

Retrying — For a condition something outside the test makes true. Both spend real time.

Check.eventually(Seatseat, Durationtimeout, Durationinterval, Consumer<Seat> body, Stringmsg)
Check.eventuallyTrue(Seatseat, Durationtimeout, BooleanSupplierpredicate, Stringmsg)

Concurrency — Reads the live non-daemon threads either side of the scope.

Check.noTaskLeaks(Seatseat, Stringmsg) -> Runnable

Testing an assertion — On Check only: Soft cannot drive a check to failure.

Check.rejects(Seatseat, Stringmsg, Consumer<Recorder> body) -> String

Golden files — recorded output, compared and rewritable.

Golden.shouldUpdate() -> booleanGolden.scrubTimestamps() -> ScrubberGolden.scrubHashes() -> ScrubberGolden.scrubRunIds() -> ScrubberGolden.scrubJsonFields(String... fields) -> ScrubberGolden.matchAt(Seatseat, Pathpath, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.match(Seatseat, Stringname, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.matchJsonField(Seatseat, Pathpath, Stringfield, Stringgot, booleanupdate, Scrubber... scrubbers)

Benchmark ceilings — chained onto one contract.

newContract(Seatseat, Stringmsg)
Contract.maxLatency(Durationceiling) -> ContractContract.maxMean(Durationceiling) -> ContractContract.maxBytes(longceiling) -> ContractContract.loop(intiterations, Raises.Bodybody) -> ContractContract.check() -> void

Coroutines — from dokimi-assert-kotlin, for the six a Java signature cannot reach.

Check.honoursCancellation(seat:Seat, msg:String, body:suspend () ->Unit)
Check.honoursDeadline(seat:Seat, msg:String, body:suspend () ->Unit)
Check.completesWithin(seat:Seat, within:Duration, msg:String, body:suspend () ->Unit)
Check.eventually(seat:Seat, timeout:Duration, interval:Duration, msg:String, body:suspend (Seat) ->Unit)
Check.eventuallyTrue(seat:Seat, timeout:Duration, msg:String, predicate:suspend () ->Boolean)
Check.noTaskLeaks(seat:Seat, msg:String, body:suspend (CoroutineScope) ->Unit)

Each one carries a doc comment: what it states, what every argument means, the edge cases it decides, and a worked call.

Equality

Object.equals answers a different question in three places, and this corrects all three:

ExpressionequalsHere
Double.valueOf(NaN).equals(NaN)truenot equal, per IEEE 754
Double.valueOf(0.0).equals(-0.0)falseequal
new int[]{1}.equals(new int[]{1})falseequal
Integer.valueOf(1).equals(1L)falsenot equal, and for the right reason

Comparison is structural and reaches arrays, collections and maps, including an array nested inside a list that otherwise compares by value. Different classes never compare, and a cycle stops the walk.

Pass Option.EQUATE_NANS or Option.EQUATE_EMPTY to relax either for one call. An option applies to the call it is passed to and nothing else.

Kotlin

Kotlin calls the Java artifact for thirty-five of the forty-one. The other six take work that suspends, and a Kotlin suspend lambda compiles to a method taking a hidden Continuation, so no Java method can accept one. dokimi-assert-kotlin supplies those:

classWorkerTest {
@Test
fun`it stops when told`() = runTest {
Check.honoursCancellation(seat, "the worker stops when told") {
worker.serve()
}
}
}

Cancellation there is a coroutine's own: a Job cancelled and a CancellationException raised at the next suspension point. In the Java artifact it is Thread.interrupt, which is what sleep, wait, take and every blocking call in java.util.concurrent respond to.

The standard

The assertions are defined in assert-spec, language-neutral and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records what this language cannot supply, and what a check cannot see.

Where Java differs

bench.Contract.maxAllocs is declared rather than implemented. The JVM reports bytes allocated per thread and no count of allocations; JFR samples allocation events rather than counting them. maxBytes is implemented, and holds a ceiling on the same behaviour by weight: ThreadMXBean.getThreadAllocatedBytes counts what a thread allocated rather than what survived a collection, so the reading does not move with the collector.

noTaskLeaks sees platform threads and executor threads. A leaked virtual thread is not reported, because virtual threads appear in no standard enumeration on any JVM version. The overlay records that as a limit rather than leaving it to be discovered.

throwsException, because throws is a keyword. It takes a Body rather than a Runnable, so a caller need not wrap a method that declares a checked exception.

Development

./gradlew build # compile, test, document, jar
./gradlew test
./gradlew javadoc
./gradlew centralBundle # both artifacts as one Maven Central bundle

Building needs JDK 23 or newer, because the doc comments are Markdown and older javadoc reads /// as an ordinary comment. The artifact targets Java 17 regardless, and CI runs the tests on a real 17.

Pushing a v* tag builds a signed bundle and uploads it to the Central Portal, which validates it and waits for someone to release it. The build signs only when SIGNING_KEY and SIGNING_PASSWORD are in the environment, so an ordinary build needs no key.

docs/rfc/0001 records what Java does differently from the other implementations of this standard, and why.

Licence

MIT. See LICENSE.

About

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run.

CILicenceJava

dependencies {
testImplementation("dev.dokimi:assert-core:0.1.0")
testImplementation("dev.dokimi:assert-kotlin:0.1.0") // coroutines
}

Java 17 and up. No runtime dependencies beyond JSpecify's annotations.

Getting started

classStoreTest {
@RegisterExtensionfinalSeatExtensionseat = newSeatExtension();
@Testvoidget() {
varitem = store.get("widget");
Check.isNotNull(seat, item, "get answers the stored item");
Check.equal(seat, item.name(), "widget", "and the item is the one stored");
}
}

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionFailed: and the item is the one stored: want "widget", got "gadget"

What a seat is

The seat is where a failure goes. Assertions never call a test framework and never throw on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

SeatCheck doesSoft does
Collector, from SeatExtensionthrowscollects, thrown when the test ends
Standardthrowsthrows
Recordercollectscollects

SeatExtension is a Seat itself, so it goes straight into a call. It is a field rather than a parameter, because a parameter resolver hands out a value JUnit then forgets: nothing would be left holding the collector when the body ends. It is the only class here that mentions JUnit, and it is optional.

Two surfaces

Check stops at the first failure. Soft records and carries on, so one run shows every property that failed.

Check.equal(seat, reply.status(), 200, "the request succeeds");
Soft.hasPrefix(seat, reply.body(), "{", "the body is JSON");
Soft.length(seat, reply.items(), 3, "every item comes back");

If both Soft calls fail, both are reported together:

AssertionFailed: 2 failures:
1. the body is JSON: "[1,2]" does not start with "{"
2. every item comes back: expected length 3, got 2

Several assertions about one value

that starts a chain, so a value is named once and every method after it answers the chain:

Check.that(seat, reply.status())
.notEqual(0, "the status was set")
.equal(200, "the request succeeds");

The chain fixes the value's type, so want is held to it and a mismatch is a compile error:

Check.that(seat, reply.body()).equal(200, "..."); // does not compileCheck.equal(seat, reply.body(), 200, "..."); // compiles, fails at run time

The static form cannot be tightened the same way. Java infers a common supertype for two arguments of a generic method, so equal(seat, "1", 1, ...) would still compile whatever the parameters were declared as.

A chain from Check stops at the first failing method. One from Soft runs them all and reports each failure.

The assertions

Thirty-four on Check and thirty-three on Soft, since only Check can drive an assertion to failure. Three more compare against a golden file, and the benchmark contract states three ceilings. that starts a chain over any of the value assertions.

Every assertion takes the seat first and the message last. Check and Soft carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

Check.equal(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)
Check.notEqual(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)

Truth and absence — Java has one null, so this is simpler than the JavaScript column.

Check.isTrue(Seatseat, booleancondition, Stringmsg)
Check.isFalse(Seatseat, booleancondition, Stringmsg)
Check.isNull(Seatseat, Objectgot, Stringmsg)
Check.isNotNull(Seatseat, Objectgot, Stringmsg)

Size — A CharSequence, a Collection, a Map or an array.

Check.length(Seatseat, Objectgot, intwant, Stringmsg)
Check.isEmpty(Seatseat, Objectgot, Stringmsg)
Check.isNotEmpty(Seatseat, Objectgot, Stringmsg)

Containment — What holding means follows the haystack.

Check.contains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.notContains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.containsInOrder(Seatseat, Objectgot, String[] needles, Stringmsg)

Text — CharSequence.

Check.hasPrefix(Seatseat, Objectgot, Stringprefix, Stringmsg)
Check.hasSuffix(Seatseat, Objectgot, Stringsuffix, Stringmsg)
Check.matches(Seatseat, Objectgot, Stringpattern, Stringmsg)

Numbers — Where exact equality is the wrong question.

Check.closeTo(Seatseat, Objectgot, doublewant, doubletolerance, Stringmsg)
Check.inRange(Seatseat, Objectgot, doublelow, doublehigh, Stringmsg)

Errors — For code that hands an exception back. Matching follows the cause chain.

Check.noError(Seatseat, Throwableerror, Stringmsg)
Check.hasError(Seatseat, Throwableerror, Stringmsg)
Check.errorIs(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorIsNot(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorAs(Seatseat, Throwableerror, Class<E> want, Stringmsg) -> @NullableE

ThrowingthrowsException, because throws is a keyword.

Check.throwsException(Seatseat, Raises.Bodybody, Stringmsg) -> @NullableThrowableCheck.doesNotThrow(Seatseat, Raises.Bodybody, Stringmsg)

Ordering — Sorted, unique, and anything else that holds between neighbours.

Check.pairwise(Seatseat, List<T> items, BiPredicate<T, T> predicate, Stringmsg)

Cancellation — Interruption is Java's cancellation: what sleep, wait and take respond to.

Check.honoursCancellation(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.honoursDeadline(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.completesWithin(Seatseat, Durationwithin, Raises.Bodybody, Stringmsg)

Purity and a missing handle — What observe answers defines what nothing means.

Check.isPure(Seatseat, Callable<Object> observe, Raises.Bodybody, Stringmsg, Option... options)
Check.nullHandleSafe(Seatseat, Behaviour.Handledbody, Stringmsg)

Retrying — For a condition something outside the test makes true. Both spend real time.

Check.eventually(Seatseat, Durationtimeout, Durationinterval, Consumer<Seat> body, Stringmsg)
Check.eventuallyTrue(Seatseat, Durationtimeout, BooleanSupplierpredicate, Stringmsg)

Concurrency — Reads the live non-daemon threads either side of the scope.

Check.noTaskLeaks(Seatseat, Stringmsg) -> Runnable

Testing an assertion — On Check only: Soft cannot drive a check to failure.

Check.rejects(Seatseat, Stringmsg, Consumer<Recorder> body) -> String

Golden files — recorded output, compared and rewritable.

Golden.shouldUpdate() -> booleanGolden.scrubTimestamps() -> ScrubberGolden.scrubHashes() -> ScrubberGolden.scrubRunIds() -> ScrubberGolden.scrubJsonFields(String... fields) -> ScrubberGolden.matchAt(Seatseat, Pathpath, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.match(Seatseat, Stringname, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.matchJsonField(Seatseat, Pathpath, Stringfield, Stringgot, booleanupdate, Scrubber... scrubbers)

Benchmark ceilings — chained onto one contract.

newContract(Seatseat, Stringmsg)
Contract.maxLatency(Durationceiling) -> ContractContract.maxMean(Durationceiling) -> ContractContract.maxBytes(longceiling) -> ContractContract.loop(intiterations, Raises.Bodybody) -> ContractContract.check() -> void

Coroutines — from dokimi-assert-kotlin, for the six a Java signature cannot reach.

Check.honoursCancellation(seat:Seat, msg:String, body:suspend () ->Unit)
Check.honoursDeadline(seat:Seat, msg:String, body:suspend () ->Unit)
Check.completesWithin(seat:Seat, within:Duration, msg:String, body:suspend () ->Unit)
Check.eventually(seat:Seat, timeout:Duration, interval:Duration, msg:String, body:suspend (Seat) ->Unit)
Check.eventuallyTrue(seat:Seat, timeout:Duration, msg:String, predicate:suspend () ->Boolean)
Check.noTaskLeaks(seat:Seat, msg:String, body:suspend (CoroutineScope) ->Unit)

Each one carries a doc comment: what it states, what every argument means, the edge cases it decides, and a worked call.

Equality

Object.equals answers a different question in three places, and this corrects all three:

ExpressionequalsHere
Double.valueOf(NaN).equals(NaN)truenot equal, per IEEE 754
Double.valueOf(0.0).equals(-0.0)falseequal
new int[]{1}.equals(new int[]{1})falseequal
Integer.valueOf(1).equals(1L)falsenot equal, and for the right reason

Comparison is structural and reaches arrays, collections and maps, including an array nested inside a list that otherwise compares by value. Different classes never compare, and a cycle stops the walk.

Pass Option.EQUATE_NANS or Option.EQUATE_EMPTY to relax either for one call. An option applies to the call it is passed to and nothing else.

Kotlin

Kotlin calls the Java artifact for thirty-five of the forty-one. The other six take work that suspends, and a Kotlin suspend lambda compiles to a method taking a hidden Continuation, so no Java method can accept one. dokimi-assert-kotlin supplies those:

classWorkerTest {
@Test
fun`it stops when told`() = runTest {
Check.honoursCancellation(seat, "the worker stops when told") {
worker.serve()
}
}
}

Cancellation there is a coroutine's own: a Job cancelled and a CancellationException raised at the next suspension point. In the Java artifact it is Thread.interrupt, which is what sleep, wait, take and every blocking call in java.util.concurrent respond to.

The standard

The assertions are defined in assert-spec, language-neutral and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records what this language cannot supply, and what a check cannot see.

Where Java differs

bench.Contract.maxAllocs is declared rather than implemented. The JVM reports bytes allocated per thread and no count of allocations; JFR samples allocation events rather than counting them. maxBytes is implemented, and holds a ceiling on the same behaviour by weight: ThreadMXBean.getThreadAllocatedBytes counts what a thread allocated rather than what survived a collection, so the reading does not move with the collector.

noTaskLeaks sees platform threads and executor threads. A leaked virtual thread is not reported, because virtual threads appear in no standard enumeration on any JVM version. The overlay records that as a limit rather than leaving it to be discovered.

throwsException, because throws is a keyword. It takes a Body rather than a Runnable, so a caller need not wrap a method that declares a checked exception.

Development

./gradlew build # compile, test, document, jar
./gradlew test
./gradlew javadoc
./gradlew centralBundle # both artifacts as one Maven Central bundle

Building needs JDK 23 or newer, because the doc comments are Markdown and older javadoc reads /// as an ordinary comment. The artifact targets Java 17 regardless, and CI runs the tests on a real 17.

Pushing a v* tag builds a signed bundle and uploads it to the Central Portal, which validates it and waits for someone to release it. The build signs only when SIGNING_KEY and SIGNING_PASSWORD are in the environment, so an ordinary build needs no key.

docs/rfc/0001 records what Java does differently from the other implementations of this standard, and why.

Licence

MIT. See LICENSE.

About

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run.

CILicenceJava

dependencies {
testImplementation("dev.dokimi:assert-core:0.1.0")
testImplementation("dev.dokimi:assert-kotlin:0.1.0") // coroutines
}

Java 17 and up. No runtime dependencies beyond JSpecify's annotations.

Getting started

classStoreTest {
@RegisterExtensionfinalSeatExtensionseat = newSeatExtension();
@Testvoidget() {
varitem = store.get("widget");
Check.isNotNull(seat, item, "get answers the stored item");
Check.equal(seat, item.name(), "widget", "and the item is the one stored");
}
}

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionFailed: and the item is the one stored: want "widget", got "gadget"

What a seat is

The seat is where a failure goes. Assertions never call a test framework and never throw on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

SeatCheck doesSoft does
Collector, from SeatExtensionthrowscollects, thrown when the test ends
Standardthrowsthrows
Recordercollectscollects

SeatExtension is a Seat itself, so it goes straight into a call. It is a field rather than a parameter, because a parameter resolver hands out a value JUnit then forgets: nothing would be left holding the collector when the body ends. It is the only class here that mentions JUnit, and it is optional.

Two surfaces

Check stops at the first failure. Soft records and carries on, so one run shows every property that failed.

Check.equal(seat, reply.status(), 200, "the request succeeds");
Soft.hasPrefix(seat, reply.body(), "{", "the body is JSON");
Soft.length(seat, reply.items(), 3, "every item comes back");

If both Soft calls fail, both are reported together:

AssertionFailed: 2 failures:
1. the body is JSON: "[1,2]" does not start with "{"
2. every item comes back: expected length 3, got 2

Several assertions about one value

that starts a chain, so a value is named once and every method after it answers the chain:

Check.that(seat, reply.status())
.notEqual(0, "the status was set")
.equal(200, "the request succeeds");

The chain fixes the value's type, so want is held to it and a mismatch is a compile error:

Check.that(seat, reply.body()).equal(200, "..."); // does not compileCheck.equal(seat, reply.body(), 200, "..."); // compiles, fails at run time

The static form cannot be tightened the same way. Java infers a common supertype for two arguments of a generic method, so equal(seat, "1", 1, ...) would still compile whatever the parameters were declared as.

A chain from Check stops at the first failing method. One from Soft runs them all and reports each failure.

The assertions

Thirty-four on Check and thirty-three on Soft, since only Check can drive an assertion to failure. Three more compare against a golden file, and the benchmark contract states three ceilings. that starts a chain over any of the value assertions.

Every assertion takes the seat first and the message last. Check and Soft carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

Check.equal(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)
Check.notEqual(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)

Truth and absence — Java has one null, so this is simpler than the JavaScript column.

Check.isTrue(Seatseat, booleancondition, Stringmsg)
Check.isFalse(Seatseat, booleancondition, Stringmsg)
Check.isNull(Seatseat, Objectgot, Stringmsg)
Check.isNotNull(Seatseat, Objectgot, Stringmsg)

Size — A CharSequence, a Collection, a Map or an array.

Check.length(Seatseat, Objectgot, intwant, Stringmsg)
Check.isEmpty(Seatseat, Objectgot, Stringmsg)
Check.isNotEmpty(Seatseat, Objectgot, Stringmsg)

Containment — What holding means follows the haystack.

Check.contains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.notContains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.containsInOrder(Seatseat, Objectgot, String[] needles, Stringmsg)

Text — CharSequence.

Check.hasPrefix(Seatseat, Objectgot, Stringprefix, Stringmsg)
Check.hasSuffix(Seatseat, Objectgot, Stringsuffix, Stringmsg)
Check.matches(Seatseat, Objectgot, Stringpattern, Stringmsg)

Numbers — Where exact equality is the wrong question.

Check.closeTo(Seatseat, Objectgot, doublewant, doubletolerance, Stringmsg)
Check.inRange(Seatseat, Objectgot, doublelow, doublehigh, Stringmsg)

Errors — For code that hands an exception back. Matching follows the cause chain.

Check.noError(Seatseat, Throwableerror, Stringmsg)
Check.hasError(Seatseat, Throwableerror, Stringmsg)
Check.errorIs(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorIsNot(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorAs(Seatseat, Throwableerror, Class<E> want, Stringmsg) -> @NullableE

ThrowingthrowsException, because throws is a keyword.

Check.throwsException(Seatseat, Raises.Bodybody, Stringmsg) -> @NullableThrowableCheck.doesNotThrow(Seatseat, Raises.Bodybody, Stringmsg)

Ordering — Sorted, unique, and anything else that holds between neighbours.

Check.pairwise(Seatseat, List<T> items, BiPredicate<T, T> predicate, Stringmsg)

Cancellation — Interruption is Java's cancellation: what sleep, wait and take respond to.

Check.honoursCancellation(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.honoursDeadline(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.completesWithin(Seatseat, Durationwithin, Raises.Bodybody, Stringmsg)

Purity and a missing handle — What observe answers defines what nothing means.

Check.isPure(Seatseat, Callable<Object> observe, Raises.Bodybody, Stringmsg, Option... options)
Check.nullHandleSafe(Seatseat, Behaviour.Handledbody, Stringmsg)

Retrying — For a condition something outside the test makes true. Both spend real time.

Check.eventually(Seatseat, Durationtimeout, Durationinterval, Consumer<Seat> body, Stringmsg)
Check.eventuallyTrue(Seatseat, Durationtimeout, BooleanSupplierpredicate, Stringmsg)

Concurrency — Reads the live non-daemon threads either side of the scope.

Check.noTaskLeaks(Seatseat, Stringmsg) -> Runnable

Testing an assertion — On Check only: Soft cannot drive a check to failure.

Check.rejects(Seatseat, Stringmsg, Consumer<Recorder> body) -> String

Golden files — recorded output, compared and rewritable.

Golden.shouldUpdate() -> booleanGolden.scrubTimestamps() -> ScrubberGolden.scrubHashes() -> ScrubberGolden.scrubRunIds() -> ScrubberGolden.scrubJsonFields(String... fields) -> ScrubberGolden.matchAt(Seatseat, Pathpath, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.match(Seatseat, Stringname, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.matchJsonField(Seatseat, Pathpath, Stringfield, Stringgot, booleanupdate, Scrubber... scrubbers)

Benchmark ceilings — chained onto one contract.

newContract(Seatseat, Stringmsg)
Contract.maxLatency(Durationceiling) -> ContractContract.maxMean(Durationceiling) -> ContractContract.maxBytes(longceiling) -> ContractContract.loop(intiterations, Raises.Bodybody) -> ContractContract.check() -> void

Coroutines — from dokimi-assert-kotlin, for the six a Java signature cannot reach.

Check.honoursCancellation(seat:Seat, msg:String, body:suspend () ->Unit)
Check.honoursDeadline(seat:Seat, msg:String, body:suspend () ->Unit)
Check.completesWithin(seat:Seat, within:Duration, msg:String, body:suspend () ->Unit)
Check.eventually(seat:Seat, timeout:Duration, interval:Duration, msg:String, body:suspend (Seat) ->Unit)
Check.eventuallyTrue(seat:Seat, timeout:Duration, msg:String, predicate:suspend () ->Boolean)
Check.noTaskLeaks(seat:Seat, msg:String, body:suspend (CoroutineScope) ->Unit)

Each one carries a doc comment: what it states, what every argument means, the edge cases it decides, and a worked call.

Equality

Object.equals answers a different question in three places, and this corrects all three:

ExpressionequalsHere
Double.valueOf(NaN).equals(NaN)truenot equal, per IEEE 754
Double.valueOf(0.0).equals(-0.0)falseequal
new int[]{1}.equals(new int[]{1})falseequal
Integer.valueOf(1).equals(1L)falsenot equal, and for the right reason

Comparison is structural and reaches arrays, collections and maps, including an array nested inside a list that otherwise compares by value. Different classes never compare, and a cycle stops the walk.

Pass Option.EQUATE_NANS or Option.EQUATE_EMPTY to relax either for one call. An option applies to the call it is passed to and nothing else.

Kotlin

Kotlin calls the Java artifact for thirty-five of the forty-one. The other six take work that suspends, and a Kotlin suspend lambda compiles to a method taking a hidden Continuation, so no Java method can accept one. dokimi-assert-kotlin supplies those:

classWorkerTest {
@Test
fun`it stops when told`() = runTest {
Check.honoursCancellation(seat, "the worker stops when told") {
worker.serve()
}
}
}

Cancellation there is a coroutine's own: a Job cancelled and a CancellationException raised at the next suspension point. In the Java artifact it is Thread.interrupt, which is what sleep, wait, take and every blocking call in java.util.concurrent respond to.

The standard

The assertions are defined in assert-spec, language-neutral and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records what this language cannot supply, and what a check cannot see.

Where Java differs

bench.Contract.maxAllocs is declared rather than implemented. The JVM reports bytes allocated per thread and no count of allocations; JFR samples allocation events rather than counting them. maxBytes is implemented, and holds a ceiling on the same behaviour by weight: ThreadMXBean.getThreadAllocatedBytes counts what a thread allocated rather than what survived a collection, so the reading does not move with the collector.

noTaskLeaks sees platform threads and executor threads. A leaked virtual thread is not reported, because virtual threads appear in no standard enumeration on any JVM version. The overlay records that as a limit rather than leaving it to be discovered.

throwsException, because throws is a keyword. It takes a Body rather than a Runnable, so a caller need not wrap a method that declares a checked exception.

Development

./gradlew build # compile, test, document, jar
./gradlew test
./gradlew javadoc
./gradlew centralBundle # both artifacts as one Maven Central bundle

Building needs JDK 23 or newer, because the doc comments are Markdown and older javadoc reads /// as an ordinary comment. The artifact targets Java 17 regardless, and CI runs the tests on a real 17.

Pushing a v* tag builds a signed bundle and uploads it to the Central Portal, which validates it and waits for someone to release it. The build signs only when SIGNING_KEY and SIGNING_PASSWORD are in the environment, so an ordinary build needs no key.

docs/rfc/0001 records what Java does differently from the other implementations of this standard, and why.

Licence

MIT. See LICENSE.

About

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run.

CILicenceJava

dependencies {
testImplementation("dev.dokimi:assert-core:0.1.0")
testImplementation("dev.dokimi:assert-kotlin:0.1.0") // coroutines
}

Java 17 and up. No runtime dependencies beyond JSpecify's annotations.

Getting started

classStoreTest {
@RegisterExtensionfinalSeatExtensionseat = newSeatExtension();
@Testvoidget() {
varitem = store.get("widget");
Check.isNotNull(seat, item, "get answers the stored item");
Check.equal(seat, item.name(), "widget", "and the item is the one stored");
}
}

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionFailed: and the item is the one stored: want "widget", got "gadget"

What a seat is

The seat is where a failure goes. Assertions never call a test framework and never throw on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

SeatCheck doesSoft does
Collector, from SeatExtensionthrowscollects, thrown when the test ends
Standardthrowsthrows
Recordercollectscollects

SeatExtension is a Seat itself, so it goes straight into a call. It is a field rather than a parameter, because a parameter resolver hands out a value JUnit then forgets: nothing would be left holding the collector when the body ends. It is the only class here that mentions JUnit, and it is optional.

Two surfaces

Check stops at the first failure. Soft records and carries on, so one run shows every property that failed.

Check.equal(seat, reply.status(), 200, "the request succeeds");
Soft.hasPrefix(seat, reply.body(), "{", "the body is JSON");
Soft.length(seat, reply.items(), 3, "every item comes back");

If both Soft calls fail, both are reported together:

AssertionFailed: 2 failures:
1. the body is JSON: "[1,2]" does not start with "{"
2. every item comes back: expected length 3, got 2

Several assertions about one value

that starts a chain, so a value is named once and every method after it answers the chain:

Check.that(seat, reply.status())
.notEqual(0, "the status was set")
.equal(200, "the request succeeds");

The chain fixes the value's type, so want is held to it and a mismatch is a compile error:

Check.that(seat, reply.body()).equal(200, "..."); // does not compileCheck.equal(seat, reply.body(), 200, "..."); // compiles, fails at run time

The static form cannot be tightened the same way. Java infers a common supertype for two arguments of a generic method, so equal(seat, "1", 1, ...) would still compile whatever the parameters were declared as.

A chain from Check stops at the first failing method. One from Soft runs them all and reports each failure.

The assertions

Thirty-four on Check and thirty-three on Soft, since only Check can drive an assertion to failure. Three more compare against a golden file, and the benchmark contract states three ceilings. that starts a chain over any of the value assertions.

Every assertion takes the seat first and the message last. Check and Soft carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

Check.equal(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)
Check.notEqual(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)

Truth and absence — Java has one null, so this is simpler than the JavaScript column.

Check.isTrue(Seatseat, booleancondition, Stringmsg)
Check.isFalse(Seatseat, booleancondition, Stringmsg)
Check.isNull(Seatseat, Objectgot, Stringmsg)
Check.isNotNull(Seatseat, Objectgot, Stringmsg)

Size — A CharSequence, a Collection, a Map or an array.

Check.length(Seatseat, Objectgot, intwant, Stringmsg)
Check.isEmpty(Seatseat, Objectgot, Stringmsg)
Check.isNotEmpty(Seatseat, Objectgot, Stringmsg)

Containment — What holding means follows the haystack.

Check.contains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.notContains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.containsInOrder(Seatseat, Objectgot, String[] needles, Stringmsg)

Text — CharSequence.

Check.hasPrefix(Seatseat, Objectgot, Stringprefix, Stringmsg)
Check.hasSuffix(Seatseat, Objectgot, Stringsuffix, Stringmsg)
Check.matches(Seatseat, Objectgot, Stringpattern, Stringmsg)

Numbers — Where exact equality is the wrong question.

Check.closeTo(Seatseat, Objectgot, doublewant, doubletolerance, Stringmsg)
Check.inRange(Seatseat, Objectgot, doublelow, doublehigh, Stringmsg)

Errors — For code that hands an exception back. Matching follows the cause chain.

Check.noError(Seatseat, Throwableerror, Stringmsg)
Check.hasError(Seatseat, Throwableerror, Stringmsg)
Check.errorIs(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorIsNot(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorAs(Seatseat, Throwableerror, Class<E> want, Stringmsg) -> @NullableE

ThrowingthrowsException, because throws is a keyword.

Check.throwsException(Seatseat, Raises.Bodybody, Stringmsg) -> @NullableThrowableCheck.doesNotThrow(Seatseat, Raises.Bodybody, Stringmsg)

Ordering — Sorted, unique, and anything else that holds between neighbours.

Check.pairwise(Seatseat, List<T> items, BiPredicate<T, T> predicate, Stringmsg)

Cancellation — Interruption is Java's cancellation: what sleep, wait and take respond to.

Check.honoursCancellation(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.honoursDeadline(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.completesWithin(Seatseat, Durationwithin, Raises.Bodybody, Stringmsg)

Purity and a missing handle — What observe answers defines what nothing means.

Check.isPure(Seatseat, Callable<Object> observe, Raises.Bodybody, Stringmsg, Option... options)
Check.nullHandleSafe(Seatseat, Behaviour.Handledbody, Stringmsg)

Retrying — For a condition something outside the test makes true. Both spend real time.

Check.eventually(Seatseat, Durationtimeout, Durationinterval, Consumer<Seat> body, Stringmsg)
Check.eventuallyTrue(Seatseat, Durationtimeout, BooleanSupplierpredicate, Stringmsg)

Concurrency — Reads the live non-daemon threads either side of the scope.

Check.noTaskLeaks(Seatseat, Stringmsg) -> Runnable

Testing an assertion — On Check only: Soft cannot drive a check to failure.

Check.rejects(Seatseat, Stringmsg, Consumer<Recorder> body) -> String

Golden files — recorded output, compared and rewritable.

Golden.shouldUpdate() -> booleanGolden.scrubTimestamps() -> ScrubberGolden.scrubHashes() -> ScrubberGolden.scrubRunIds() -> ScrubberGolden.scrubJsonFields(String... fields) -> ScrubberGolden.matchAt(Seatseat, Pathpath, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.match(Seatseat, Stringname, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.matchJsonField(Seatseat, Pathpath, Stringfield, Stringgot, booleanupdate, Scrubber... scrubbers)

Benchmark ceilings — chained onto one contract.

newContract(Seatseat, Stringmsg)
Contract.maxLatency(Durationceiling) -> ContractContract.maxMean(Durationceiling) -> ContractContract.maxBytes(longceiling) -> ContractContract.loop(intiterations, Raises.Bodybody) -> ContractContract.check() -> void

Coroutines — from dokimi-assert-kotlin, for the six a Java signature cannot reach.

Check.honoursCancellation(seat:Seat, msg:String, body:suspend () ->Unit)
Check.honoursDeadline(seat:Seat, msg:String, body:suspend () ->Unit)
Check.completesWithin(seat:Seat, within:Duration, msg:String, body:suspend () ->Unit)
Check.eventually(seat:Seat, timeout:Duration, interval:Duration, msg:String, body:suspend (Seat) ->Unit)
Check.eventuallyTrue(seat:Seat, timeout:Duration, msg:String, predicate:suspend () ->Boolean)
Check.noTaskLeaks(seat:Seat, msg:String, body:suspend (CoroutineScope) ->Unit)

Each one carries a doc comment: what it states, what every argument means, the edge cases it decides, and a worked call.

Equality

Object.equals answers a different question in three places, and this corrects all three:

ExpressionequalsHere
Double.valueOf(NaN).equals(NaN)truenot equal, per IEEE 754
Double.valueOf(0.0).equals(-0.0)falseequal
new int[]{1}.equals(new int[]{1})falseequal
Integer.valueOf(1).equals(1L)falsenot equal, and for the right reason

Comparison is structural and reaches arrays, collections and maps, including an array nested inside a list that otherwise compares by value. Different classes never compare, and a cycle stops the walk.

Pass Option.EQUATE_NANS or Option.EQUATE_EMPTY to relax either for one call. An option applies to the call it is passed to and nothing else.

Kotlin

Kotlin calls the Java artifact for thirty-five of the forty-one. The other six take work that suspends, and a Kotlin suspend lambda compiles to a method taking a hidden Continuation, so no Java method can accept one. dokimi-assert-kotlin supplies those:

classWorkerTest {
@Test
fun`it stops when told`() = runTest {
Check.honoursCancellation(seat, "the worker stops when told") {
worker.serve()
}
}
}

Cancellation there is a coroutine's own: a Job cancelled and a CancellationException raised at the next suspension point. In the Java artifact it is Thread.interrupt, which is what sleep, wait, take and every blocking call in java.util.concurrent respond to.

The standard

The assertions are defined in assert-spec, language-neutral and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records what this language cannot supply, and what a check cannot see.

Where Java differs

bench.Contract.maxAllocs is declared rather than implemented. The JVM reports bytes allocated per thread and no count of allocations; JFR samples allocation events rather than counting them. maxBytes is implemented, and holds a ceiling on the same behaviour by weight: ThreadMXBean.getThreadAllocatedBytes counts what a thread allocated rather than what survived a collection, so the reading does not move with the collector.

noTaskLeaks sees platform threads and executor threads. A leaked virtual thread is not reported, because virtual threads appear in no standard enumeration on any JVM version. The overlay records that as a limit rather than leaving it to be discovered.

throwsException, because throws is a keyword. It takes a Body rather than a Runnable, so a caller need not wrap a method that declares a checked exception.

Development

./gradlew build # compile, test, document, jar
./gradlew test
./gradlew javadoc
./gradlew centralBundle # both artifacts as one Maven Central bundle

Building needs JDK 23 or newer, because the doc comments are Markdown and older javadoc reads /// as an ordinary comment. The artifact targets Java 17 regardless, and CI runs the tests on a real 17.

Pushing a v* tag builds a signed bundle and uploads it to the Central Portal, which validates it and waits for someone to release it. The build signs only when SIGNING_KEY and SIGNING_PASSWORD are in the environment, so an ordinary build needs no key.

docs/rfc/0001 records what Java does differently from the other implementations of this standard, and why.

Licence

MIT. See LICENSE.

About

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run.

CILicenceJava

dependencies {
testImplementation("dev.dokimi:assert-core:0.1.0")
testImplementation("dev.dokimi:assert-kotlin:0.1.0") // coroutines
}

Java 17 and up. No runtime dependencies beyond JSpecify's annotations.

Getting started

classStoreTest {
@RegisterExtensionfinalSeatExtensionseat = newSeatExtension();
@Testvoidget() {
varitem = store.get("widget");
Check.isNotNull(seat, item, "get answers the stored item");
Check.equal(seat, item.name(), "widget", "and the item is the one stored");
}
}

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionFailed: and the item is the one stored: want "widget", got "gadget"

What a seat is

The seat is where a failure goes. Assertions never call a test framework and never throw on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

SeatCheck doesSoft does
Collector, from SeatExtensionthrowscollects, thrown when the test ends
Standardthrowsthrows
Recordercollectscollects

SeatExtension is a Seat itself, so it goes straight into a call. It is a field rather than a parameter, because a parameter resolver hands out a value JUnit then forgets: nothing would be left holding the collector when the body ends. It is the only class here that mentions JUnit, and it is optional.

Two surfaces

Check stops at the first failure. Soft records and carries on, so one run shows every property that failed.

Check.equal(seat, reply.status(), 200, "the request succeeds");
Soft.hasPrefix(seat, reply.body(), "{", "the body is JSON");
Soft.length(seat, reply.items(), 3, "every item comes back");

If both Soft calls fail, both are reported together:

AssertionFailed: 2 failures:
1. the body is JSON: "[1,2]" does not start with "{"
2. every item comes back: expected length 3, got 2

Several assertions about one value

that starts a chain, so a value is named once and every method after it answers the chain:

Check.that(seat, reply.status())
.notEqual(0, "the status was set")
.equal(200, "the request succeeds");

The chain fixes the value's type, so want is held to it and a mismatch is a compile error:

Check.that(seat, reply.body()).equal(200, "..."); // does not compileCheck.equal(seat, reply.body(), 200, "..."); // compiles, fails at run time

The static form cannot be tightened the same way. Java infers a common supertype for two arguments of a generic method, so equal(seat, "1", 1, ...) would still compile whatever the parameters were declared as.

A chain from Check stops at the first failing method. One from Soft runs them all and reports each failure.

The assertions

Thirty-four on Check and thirty-three on Soft, since only Check can drive an assertion to failure. Three more compare against a golden file, and the benchmark contract states three ceilings. that starts a chain over any of the value assertions.

Every assertion takes the seat first and the message last. Check and Soft carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

Check.equal(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)
Check.notEqual(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)

Truth and absence — Java has one null, so this is simpler than the JavaScript column.

Check.isTrue(Seatseat, booleancondition, Stringmsg)
Check.isFalse(Seatseat, booleancondition, Stringmsg)
Check.isNull(Seatseat, Objectgot, Stringmsg)
Check.isNotNull(Seatseat, Objectgot, Stringmsg)

Size — A CharSequence, a Collection, a Map or an array.

Check.length(Seatseat, Objectgot, intwant, Stringmsg)
Check.isEmpty(Seatseat, Objectgot, Stringmsg)
Check.isNotEmpty(Seatseat, Objectgot, Stringmsg)

Containment — What holding means follows the haystack.

Check.contains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.notContains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.containsInOrder(Seatseat, Objectgot, String[] needles, Stringmsg)

Text — CharSequence.

Check.hasPrefix(Seatseat, Objectgot, Stringprefix, Stringmsg)
Check.hasSuffix(Seatseat, Objectgot, Stringsuffix, Stringmsg)
Check.matches(Seatseat, Objectgot, Stringpattern, Stringmsg)

Numbers — Where exact equality is the wrong question.

Check.closeTo(Seatseat, Objectgot, doublewant, doubletolerance, Stringmsg)
Check.inRange(Seatseat, Objectgot, doublelow, doublehigh, Stringmsg)

Errors — For code that hands an exception back. Matching follows the cause chain.

Check.noError(Seatseat, Throwableerror, Stringmsg)
Check.hasError(Seatseat, Throwableerror, Stringmsg)
Check.errorIs(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorIsNot(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorAs(Seatseat, Throwableerror, Class<E> want, Stringmsg) -> @NullableE

ThrowingthrowsException, because throws is a keyword.

Check.throwsException(Seatseat, Raises.Bodybody, Stringmsg) -> @NullableThrowableCheck.doesNotThrow(Seatseat, Raises.Bodybody, Stringmsg)

Ordering — Sorted, unique, and anything else that holds between neighbours.

Check.pairwise(Seatseat, List<T> items, BiPredicate<T, T> predicate, Stringmsg)

Cancellation — Interruption is Java's cancellation: what sleep, wait and take respond to.

Check.honoursCancellation(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.honoursDeadline(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.completesWithin(Seatseat, Durationwithin, Raises.Bodybody, Stringmsg)

Purity and a missing handle — What observe answers defines what nothing means.

Check.isPure(Seatseat, Callable<Object> observe, Raises.Bodybody, Stringmsg, Option... options)
Check.nullHandleSafe(Seatseat, Behaviour.Handledbody, Stringmsg)

Retrying — For a condition something outside the test makes true. Both spend real time.

Check.eventually(Seatseat, Durationtimeout, Durationinterval, Consumer<Seat> body, Stringmsg)
Check.eventuallyTrue(Seatseat, Durationtimeout, BooleanSupplierpredicate, Stringmsg)

Concurrency — Reads the live non-daemon threads either side of the scope.

Check.noTaskLeaks(Seatseat, Stringmsg) -> Runnable

Testing an assertion — On Check only: Soft cannot drive a check to failure.

Check.rejects(Seatseat, Stringmsg, Consumer<Recorder> body) -> String

Golden files — recorded output, compared and rewritable.

Golden.shouldUpdate() -> booleanGolden.scrubTimestamps() -> ScrubberGolden.scrubHashes() -> ScrubberGolden.scrubRunIds() -> ScrubberGolden.scrubJsonFields(String... fields) -> ScrubberGolden.matchAt(Seatseat, Pathpath, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.match(Seatseat, Stringname, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.matchJsonField(Seatseat, Pathpath, Stringfield, Stringgot, booleanupdate, Scrubber... scrubbers)

Benchmark ceilings — chained onto one contract.

newContract(Seatseat, Stringmsg)
Contract.maxLatency(Durationceiling) -> ContractContract.maxMean(Durationceiling) -> ContractContract.maxBytes(longceiling) -> ContractContract.loop(intiterations, Raises.Bodybody) -> ContractContract.check() -> void

Coroutines — from dokimi-assert-kotlin, for the six a Java signature cannot reach.

Check.honoursCancellation(seat:Seat, msg:String, body:suspend () ->Unit)
Check.honoursDeadline(seat:Seat, msg:String, body:suspend () ->Unit)
Check.completesWithin(seat:Seat, within:Duration, msg:String, body:suspend () ->Unit)
Check.eventually(seat:Seat, timeout:Duration, interval:Duration, msg:String, body:suspend (Seat) ->Unit)
Check.eventuallyTrue(seat:Seat, timeout:Duration, msg:String, predicate:suspend () ->Boolean)
Check.noTaskLeaks(seat:Seat, msg:String, body:suspend (CoroutineScope) ->Unit)

Each one carries a doc comment: what it states, what every argument means, the edge cases it decides, and a worked call.

Equality

Object.equals answers a different question in three places, and this corrects all three:

ExpressionequalsHere
Double.valueOf(NaN).equals(NaN)truenot equal, per IEEE 754
Double.valueOf(0.0).equals(-0.0)falseequal
new int[]{1}.equals(new int[]{1})falseequal
Integer.valueOf(1).equals(1L)falsenot equal, and for the right reason

Comparison is structural and reaches arrays, collections and maps, including an array nested inside a list that otherwise compares by value. Different classes never compare, and a cycle stops the walk.

Pass Option.EQUATE_NANS or Option.EQUATE_EMPTY to relax either for one call. An option applies to the call it is passed to and nothing else.

Kotlin

Kotlin calls the Java artifact for thirty-five of the forty-one. The other six take work that suspends, and a Kotlin suspend lambda compiles to a method taking a hidden Continuation, so no Java method can accept one. dokimi-assert-kotlin supplies those:

classWorkerTest {
@Test
fun`it stops when told`() = runTest {
Check.honoursCancellation(seat, "the worker stops when told") {
worker.serve()
}
}
}

Cancellation there is a coroutine's own: a Job cancelled and a CancellationException raised at the next suspension point. In the Java artifact it is Thread.interrupt, which is what sleep, wait, take and every blocking call in java.util.concurrent respond to.

The standard

The assertions are defined in assert-spec, language-neutral and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records what this language cannot supply, and what a check cannot see.

Where Java differs

bench.Contract.maxAllocs is declared rather than implemented. The JVM reports bytes allocated per thread and no count of allocations; JFR samples allocation events rather than counting them. maxBytes is implemented, and holds a ceiling on the same behaviour by weight: ThreadMXBean.getThreadAllocatedBytes counts what a thread allocated rather than what survived a collection, so the reading does not move with the collector.

noTaskLeaks sees platform threads and executor threads. A leaked virtual thread is not reported, because virtual threads appear in no standard enumeration on any JVM version. The overlay records that as a limit rather than leaving it to be discovered.

throwsException, because throws is a keyword. It takes a Body rather than a Runnable, so a caller need not wrap a method that declares a checked exception.

Development

./gradlew build # compile, test, document, jar
./gradlew test
./gradlew javadoc
./gradlew centralBundle # both artifacts as one Maven Central bundle

Building needs JDK 23 or newer, because the doc comments are Markdown and older javadoc reads /// as an ordinary comment. The artifact targets Java 17 regardless, and CI runs the tests on a real 17.

Pushing a v* tag builds a signed bundle and uploads it to the Central Portal, which validates it and waits for someone to release it. The build signs only when SIGNING_KEY and SIGNING_PASSWORD are in the environment, so an ordinary build needs no key.

docs/rfc/0001 records what Java does differently from the other implementations of this standard, and why.

Licence

MIT. See LICENSE.

About

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

dokimi-assert

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run.

CILicenceJava

dependencies {
testImplementation("dev.dokimi:assert-core:0.1.0")
testImplementation("dev.dokimi:assert-kotlin:0.1.0") // coroutines
}

Java 17 and up. No runtime dependencies beyond JSpecify's annotations.

Getting started

classStoreTest {
@RegisterExtensionfinalSeatExtensionseat = newSeatExtension();
@Testvoidget() {
varitem = store.get("widget");
Check.isNotNull(seat, item, "get answers the stored item");
Check.equal(seat, item.name(), "widget", "and the item is the one stored");
}
}

Every assertion takes the seat first and a message last. The message states the contract under test and is the first line of the failure:

AssertionFailed: and the item is the one stored: want "widget", got "gadget"

What a seat is

The seat is where a failure goes. Assertions never call a test framework and never throw on their own; they report to whatever seat they are handed. That is what lets one assertion serve a real test, a benchmark, and a test that checks the assertion itself.

SeatCheck doesSoft does
Collector, from SeatExtensionthrowscollects, thrown when the test ends
Standardthrowsthrows
Recordercollectscollects

SeatExtension is a Seat itself, so it goes straight into a call. It is a field rather than a parameter, because a parameter resolver hands out a value JUnit then forgets: nothing would be left holding the collector when the body ends. It is the only class here that mentions JUnit, and it is optional.

Two surfaces

Check stops at the first failure. Soft records and carries on, so one run shows every property that failed.

Check.equal(seat, reply.status(), 200, "the request succeeds");
Soft.hasPrefix(seat, reply.body(), "{", "the body is JSON");
Soft.length(seat, reply.items(), 3, "every item comes back");

If both Soft calls fail, both are reported together:

AssertionFailed: 2 failures:
1. the body is JSON: "[1,2]" does not start with "{"
2. every item comes back: expected length 3, got 2

Several assertions about one value

that starts a chain, so a value is named once and every method after it answers the chain:

Check.that(seat, reply.status())
.notEqual(0, "the status was set")
.equal(200, "the request succeeds");

The chain fixes the value's type, so want is held to it and a mismatch is a compile error:

Check.that(seat, reply.body()).equal(200, "..."); // does not compileCheck.equal(seat, reply.body(), 200, "..."); // compiles, fails at run time

The static form cannot be tightened the same way. Java infers a common supertype for two arguments of a generic method, so equal(seat, "1", 1, ...) would still compile whatever the parameters were declared as.

A chain from Check stops at the first failing method. One from Soft runs them all and reports each failure.

The assertions

Thirty-four on Check and thirty-three on Soft, since only Check can drive an assertion to failure. Three more compare against a golden file, and the benchmark contract states three ceilings. that starts a chain over any of the value assertions.

Every assertion takes the seat first and the message last. Check and Soft carry the same names and the same signatures; only what happens on a failure differs.

Equality — Structural, and strict about types.

Check.equal(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)
Check.notEqual(Seatseat, Objectgot, Objectwant, Stringmsg, Option... options)

Truth and absence — Java has one null, so this is simpler than the JavaScript column.

Check.isTrue(Seatseat, booleancondition, Stringmsg)
Check.isFalse(Seatseat, booleancondition, Stringmsg)
Check.isNull(Seatseat, Objectgot, Stringmsg)
Check.isNotNull(Seatseat, Objectgot, Stringmsg)

Size — A CharSequence, a Collection, a Map or an array.

Check.length(Seatseat, Objectgot, intwant, Stringmsg)
Check.isEmpty(Seatseat, Objectgot, Stringmsg)
Check.isNotEmpty(Seatseat, Objectgot, Stringmsg)

Containment — What holding means follows the haystack.

Check.contains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.notContains(Seatseat, Objecthaystack, Objectneedle, Stringmsg, Option... options)
Check.containsInOrder(Seatseat, Objectgot, String[] needles, Stringmsg)

Text — CharSequence.

Check.hasPrefix(Seatseat, Objectgot, Stringprefix, Stringmsg)
Check.hasSuffix(Seatseat, Objectgot, Stringsuffix, Stringmsg)
Check.matches(Seatseat, Objectgot, Stringpattern, Stringmsg)

Numbers — Where exact equality is the wrong question.

Check.closeTo(Seatseat, Objectgot, doublewant, doubletolerance, Stringmsg)
Check.inRange(Seatseat, Objectgot, doublelow, doublehigh, Stringmsg)

Errors — For code that hands an exception back. Matching follows the cause chain.

Check.noError(Seatseat, Throwableerror, Stringmsg)
Check.hasError(Seatseat, Throwableerror, Stringmsg)
Check.errorIs(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorIsNot(Seatseat, Throwableerror, Objecttarget, Stringmsg)
Check.errorAs(Seatseat, Throwableerror, Class<E> want, Stringmsg) -> @NullableE

ThrowingthrowsException, because throws is a keyword.

Check.throwsException(Seatseat, Raises.Bodybody, Stringmsg) -> @NullableThrowableCheck.doesNotThrow(Seatseat, Raises.Bodybody, Stringmsg)

Ordering — Sorted, unique, and anything else that holds between neighbours.

Check.pairwise(Seatseat, List<T> items, BiPredicate<T, T> predicate, Stringmsg)

Cancellation — Interruption is Java's cancellation: what sleep, wait and take respond to.

Check.honoursCancellation(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.honoursDeadline(Seatseat, Behaviour.Cancellablebody, Stringmsg)
Check.completesWithin(Seatseat, Durationwithin, Raises.Bodybody, Stringmsg)

Purity and a missing handle — What observe answers defines what nothing means.

Check.isPure(Seatseat, Callable<Object> observe, Raises.Bodybody, Stringmsg, Option... options)
Check.nullHandleSafe(Seatseat, Behaviour.Handledbody, Stringmsg)

Retrying — For a condition something outside the test makes true. Both spend real time.

Check.eventually(Seatseat, Durationtimeout, Durationinterval, Consumer<Seat> body, Stringmsg)
Check.eventuallyTrue(Seatseat, Durationtimeout, BooleanSupplierpredicate, Stringmsg)

Concurrency — Reads the live non-daemon threads either side of the scope.

Check.noTaskLeaks(Seatseat, Stringmsg) -> Runnable

Testing an assertion — On Check only: Soft cannot drive a check to failure.

Check.rejects(Seatseat, Stringmsg, Consumer<Recorder> body) -> String

Golden files — recorded output, compared and rewritable.

Golden.shouldUpdate() -> booleanGolden.scrubTimestamps() -> ScrubberGolden.scrubHashes() -> ScrubberGolden.scrubRunIds() -> ScrubberGolden.scrubJsonFields(String... fields) -> ScrubberGolden.matchAt(Seatseat, Pathpath, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.match(Seatseat, Stringname, Stringgot, booleanupdate, Scrubber... scrubbers)
Golden.matchJsonField(Seatseat, Pathpath, Stringfield, Stringgot, booleanupdate, Scrubber... scrubbers)

Benchmark ceilings — chained onto one contract.

newContract(Seatseat, Stringmsg)
Contract.maxLatency(Durationceiling) -> ContractContract.maxMean(Durationceiling) -> ContractContract.maxBytes(longceiling) -> ContractContract.loop(intiterations, Raises.Bodybody) -> ContractContract.check() -> void

Coroutines — from dokimi-assert-kotlin, for the six a Java signature cannot reach.

Check.honoursCancellation(seat:Seat, msg:String, body:suspend () ->Unit)
Check.honoursDeadline(seat:Seat, msg:String, body:suspend () ->Unit)
Check.completesWithin(seat:Seat, within:Duration, msg:String, body:suspend () ->Unit)
Check.eventually(seat:Seat, timeout:Duration, interval:Duration, msg:String, body:suspend (Seat) ->Unit)
Check.eventuallyTrue(seat:Seat, timeout:Duration, msg:String, predicate:suspend () ->Boolean)
Check.noTaskLeaks(seat:Seat, msg:String, body:suspend (CoroutineScope) ->Unit)

Each one carries a doc comment: what it states, what every argument means, the edge cases it decides, and a worked call.

Equality

Object.equals answers a different question in three places, and this corrects all three:

ExpressionequalsHere
Double.valueOf(NaN).equals(NaN)truenot equal, per IEEE 754
Double.valueOf(0.0).equals(-0.0)falseequal
new int[]{1}.equals(new int[]{1})falseequal
Integer.valueOf(1).equals(1L)falsenot equal, and for the right reason

Comparison is structural and reaches arrays, collections and maps, including an array nested inside a list that otherwise compares by value. Different classes never compare, and a cycle stops the walk.

Pass Option.EQUATE_NANS or Option.EQUATE_EMPTY to relax either for one call. An option applies to the call it is passed to and nothing else.

Kotlin

Kotlin calls the Java artifact for thirty-five of the forty-one. The other six take work that suspends, and a Kotlin suspend lambda compiles to a method taking a hidden Continuation, so no Java method can accept one. dokimi-assert-kotlin supplies those:

classWorkerTest {
@Test
fun`it stops when told`() = runTest {
Check.honoursCancellation(seat, "the worker stops when told") {
worker.serve()
}
}
}

Cancellation there is a coroutine's own: a Job cancelled and a CancellationException raised at the next suspension point. In the Java artifact it is Thread.interrupt, which is what sleep, wait, take and every blocking call in java.util.concurrent respond to.

The standard

The assertions are defined in assert-spec, language-neutral and implemented in several languages. This library vendors the definition and holds itself to it:

  • 87 corpus cases state what each assertion must report, run against both surfaces. They are the same cases every other implementation runs.
  • A completeness gate checks every assertion is present under the name the naming table gives it.
  • An overlay records what this language cannot supply, and what a check cannot see.

Where Java differs

bench.Contract.maxAllocs is declared rather than implemented. The JVM reports bytes allocated per thread and no count of allocations; JFR samples allocation events rather than counting them. maxBytes is implemented, and holds a ceiling on the same behaviour by weight: ThreadMXBean.getThreadAllocatedBytes counts what a thread allocated rather than what survived a collection, so the reading does not move with the collector.

noTaskLeaks sees platform threads and executor threads. A leaked virtual thread is not reported, because virtual threads appear in no standard enumeration on any JVM version. The overlay records that as a limit rather than leaving it to be discovered.

throwsException, because throws is a keyword. It takes a Body rather than a Runnable, so a caller need not wrap a method that declares a checked exception.

Development

./gradlew build # compile, test, document, jar
./gradlew test
./gradlew javadoc
./gradlew centralBundle # both artifacts as one Maven Central bundle

Building needs JDK 23 or newer, because the doc comments are Markdown and older javadoc reads /// as an ordinary comment. The artifact targets Java 17 regardless, and CI runs the tests on a real 17.

Pushing a v* tag builds a signed bundle and uploads it to the Central Portal, which validates it and waits for someone to release it. The build signs only when SIGNING_KEY and SIGNING_PASSWORD are in the environment, so an ordinary build needs no key.

docs/rfc/0001 records what Java does differently from the other implementations of this standard, and why.

Licence

MIT. See LICENSE.

About

Test assertions for Java and Kotlin, defined by a language-neutral standard and held to it on every run

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages