AssertJ Integration

Runnable example: AssertJSampleTest.java

The fun-assertj module provides fluent AssertJ custom assertions for all dmx-fun types. It is an optional test-only dependency — the core fun library has no runtime dependency on AssertJ.

Adding the dependency

fun-assertj declares assertj-core as compileOnly. You must add assertj-core explicitly to your own test classpath. Any version from 3.21.x through 3.27.x is supported (see version compatibility below).

// Gradle — test scope only
testImplementation("codes.domix:fun-assertj:0.1.0")
// AssertJ itself — bring your own version (3.21.x – 3.27.x)
testImplementation("org.assertj:assertj-core:3.27.7")
<!-- Maven — test scope only -->
<dependency>
<groupId>codes.domix</groupId>
<artifactId>fun-assertj</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>

Entry point

All assertions are accessed through a single overloaded static method:

import static dmx.fun.assertj.DmxFunAssertions.assertThat;

DmxFunAssertions.assertThat coexists with org.assertj.core.api.Assertions.assertThat — both can be statically imported in the same test class without naming conflict, because the argument types differ.

Note: fun-assertj cannot be used inside the :lib module’s own tests due to a circular dependency. Use it only in consumer projects or in the :assertj module’s own test suite.

Assertions reference

TypeMethods
Either<L, R>isRight(), isLeft(), containsRight(expected), containsLeft(expected)
Option<T>isSome(), isNone(), containsValue(expected), hasValueSatisfying(consumer)
Result<V, E>isOk(), isErr(), containsValue(expected), containsError(expected)
Try<V>isSuccess(), isFailure(), containsValue(expected), failsWith(exceptionType)
Validated<E, A>isValid(), isInvalid(), containsValue(expected), hasError(expected)
Tuple2<A, B>hasFirst(expected), hasSecond(expected)
Tuple3<A, B, C>hasFirst(expected), hasSecond(expected), hasThird(expected)
Tuple4<A, B, C, D>hasFirst(expected), hasSecond(expected), hasThird(expected), hasFourth(expected)
Resource<T>succeedsWith(expected), failsWith(exceptionType), failsWithMessage(message)
Guard<T>accepts(value), rejects(value), rejectsWithMessage(value, message), rejectsWithMessages(value, messages...)
Accumulator<E, A>hasValue(expected), hasAccumulation(expected), accumulationContains(element), accumulationHasSize(size)

Either<L, R> assertions

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
// Assert Right and its value — chainable in one expression
assertThat(Either.<String, Integer>right(42))
.isRight()
.containsRight(42);
// Assert Left and its value — chainable in one expression
assertThat(Either.<String, Integer>left("oops"))
.isLeft()
.containsLeft("oops");
// isRight() / isLeft() alone when the value does not matter
assertThat(Either.right("ok")).isRight();
assertThat(Either.left("err")).isLeft();

Option<T> assertions

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
// Assert presence and value
assertThat(Option.some(42))
.isSome()
.containsValue(42);
// Assert absence
assertThat(Option.<String>none())
.isNone();
// Assert the value satisfies a condition without extracting it
assertThat(Option.some("hello"))
.isSome()
.hasValueSatisfying(v -> assertThat(v).startsWith("hel").hasSize(5));

Result<V, E> assertions

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
// Assert Ok and its value
assertThat(Result.ok("hello"))
.isOk()
.containsValue("hello");
// Assert Err and its error
assertThat(Result.<String, String>err("oops"))
.isErr()
.containsError("oops");
// Combining with standard AssertJ — both static imports coexist
assertThat(Result.ok(List.of(1, 2, 3)))
.isOk()
.containsValue(List.of(1, 2, 3));

Try<V> assertions

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
// Assert success and value
assertThat(Try.success("data"))
.isSuccess()
.containsValue("data");
// Assert failure — just check it failed
assertThat(Try.failure(new RuntimeException("boom")))
.isFailure();
// Assert failure with a specific exception type
assertThat(Try.of(() -> { throw new IOException("disk full"); }))
.isFailure()
.failsWith(IOException.class);
// Assert a computation succeeds
assertThat(Try.of(() -> Integer.parseInt("42")))
.isSuccess()
.containsValue(42);

Validated<E, A> assertions

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
// Assert valid and value
assertThat(Validated.valid("ok"))
.isValid()
.containsValue("ok");
// Assert invalid and error
assertThat(Validated.<String, String>invalid("bad input"))
.isInvalid()
.hasError("bad input");
// Assert NEL-based validation result
Validated<NonEmptyList<String>, Integer> result =
Validated.invalidNel("must be positive");
assertThat(result)
.isInvalid()
.hasError(NonEmptyList.singleton("must be positive"));

Tuple2/3/4 assertions

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
// Tuple2
assertThat(Tuple2.of("Alice", 30))
.hasFirst("Alice")
.hasSecond(30);
// Tuple3
assertThat(Tuple3.of("Alice", 30, true))
.hasFirst("Alice")
.hasSecond(30)
.hasThird(true);
// Tuple4
assertThat(Tuple4.of("Alice", 30, true, 1.75))
.hasFirst("Alice")
.hasSecond(30)
.hasThird(true)
.hasFourth(1.75);

Resource<T> assertions

The first assertion called on a ResourceAssert runs the full acquire-use-release cycle once (via use(v -> v)) and caches the result. All subsequent chained assertions on the same instance reuse that cached Try — the lifecycle is never triggered more than once. Use succeedsWith to verify that acquisition and release complete without error; use failsWith / failsWithMessage to verify that the cycle fails in the expected way.

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
// Successful acquire + use + release
Resource<String> file = Resource.of(
() -> "file-content", // acquire
content -> { }); // release
assertThat(file).succeedsWith("file-content");
// Acquisition failure — verify exception type
Resource<String> broken = Resource.of(
() -> { throw new IOException("file not found"); },
content -> { });
assertThat(broken).failsWith(IOException.class);
// Acquisition failure — verify exception message
assertThat(broken).failsWithMessage("file not found");
// Chain both failure checks
assertThat(broken)
.failsWith(IOException.class)
.failsWithMessage("not found");
// AutoCloseable shorthand
Resource<BufferedReader> reader = Resource.fromAutoCloseable(
() -> new BufferedReader(new FileReader("data.txt")));
assertThat(reader).failsWith(FileNotFoundException.class);

Guard<T> assertions

Guard.check(value) returns Validated<NonEmptyList<String>, T>. The assertion methods call check internally — you only supply the value to validate and the messages you expect to find in the rejection error list.

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
Guard<String> alphanumeric = Guard.of(
s -> s.matches("[a-zA-Z0-9]+"), "must be alphanumeric");
// Assert acceptance
assertThat(alphanumeric).accepts("alice123");
// Assert rejection (any invalid value)
assertThat(alphanumeric).rejects("!!");
// Assert a specific rejection message is present
assertThat(alphanumeric).rejectsWithMessage("!!", "alphanumeric");
// Combined guard: assert all expected messages appear
Guard<String> minLength = Guard.of(s -> s.length() >= 3, "must be at least 3 chars");
Guard<String> username = alphanumeric.and(minLength);
assertThat(username).rejectsWithMessages("!!", "alphanumeric", "at least 3 chars");
// Fluent chain
assertThat(username)
.accepts("alice")
.rejects("!!")
.rejectsWithMessage("ab", "at least 3 chars");

Accumulator<E, A> assertions

accumulationContains and accumulationHasSize require the accumulated side-channel (E) to be a non-null java.util.Collection. If it is null or not a Collection, the assertion fails with a descriptive error. Use hasAccumulation for equality checks on any accumulation type, including non-collection and nullable values.

import static dmx.fun.assertj.DmxFunAssertions.assertThat;
BinaryOperator<List<String>> concat = (a, b) -> {
var merged = new ArrayList<>(a);
merged.addAll(b);
return merged;
};
Accumulator<List<String>, Integer> result =
Accumulator.of(10, List.of("loaded base value"))
.flatMap(v -> Accumulator.of(v * 2, List.of("doubled")), concat)
.flatMap(v -> Accumulator.of(v + 5, List.of("added 5")), concat);
// Assert the primary computed value
assertThat(result).hasValue(25);
// Assert individual log entries are present
assertThat(result)
.accumulationContains("loaded base value")
.accumulationContains("doubled")
.accumulationContains("added 5");
// Assert total number of log entries
assertThat(result).accumulationHasSize(3);
// Assert the exact accumulation
assertThat(result)
.hasAccumulation(List.of("loaded base value", "doubled", "added 5"));
// Fluent chain
assertThat(result)
.hasValue(25)
.accumulationContains("loaded base value")
.accumulationHasSize(3);

Version compatibility

The module is tested in CI against the following AssertJ versions on every pull request that touches the assertj/ module:

AssertJ versionStatus
3.21.xtested
3.22.xtested
3.23.xtested
3.24.xtested
3.25.xtested
3.26.xtested
3.27.xtested

To run the tests locally against a specific version:

Terminal window
./gradlew :assertj:test -PassertjVersion=3.25.3