diff --git a/build-all.sh b/build-all.sh
index 18c669d50..3c7e03954 100755
--- a/build-all.sh
+++ b/build-all.sh
@@ -211,6 +211,8 @@ then
build_gradle_module "spring-data/spring-data-jdbc-converter"
build_gradle_module "reactive"
build_gradle_module "junit/assumptions"
+ build_maven_module "junit/junit5/junit5"
+ build_maven_module "junit/junit5/functional-interfaces"
build_gradle_module "logging"
build_gradle_module "pact/pact-feign-consumer"
diff --git a/junit/junit5/functional-interfaces/.gitignore b/junit/junit5/functional-interfaces/.gitignore
new file mode 100644
index 000000000..57fb42c83
--- /dev/null
+++ b/junit/junit5/functional-interfaces/.gitignore
@@ -0,0 +1,35 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+.vscode
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/junit/junit5/functional-interfaces/pom.xml b/junit/junit5/functional-interfaces/pom.xml
new file mode 100644
index 000000000..5b33f1e70
--- /dev/null
+++ b/junit/junit5/functional-interfaces/pom.xml
@@ -0,0 +1,41 @@
+
+
+ 4.0.0
+
+ io.reflectoring
+ junit5-functional-interfaces
+ 1.0-SNAPSHOT
+
+
+ 17
+ 17
+ 5.9.0
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.version}
+ test
+
+
+
+ org.hamcrest
+ hamcrest-all
+ 1.3
+ test
+
+
+
+
+
diff --git a/junit/junit5/functional-interfaces/src/main/java/io/reflectoring/functional/ValidationException.java b/junit/junit5/functional-interfaces/src/main/java/io/reflectoring/functional/ValidationException.java
new file mode 100644
index 000000000..a5d4f5d36
--- /dev/null
+++ b/junit/junit5/functional-interfaces/src/main/java/io/reflectoring/functional/ValidationException.java
@@ -0,0 +1,7 @@
+package io.reflectoring.functional;
+
+public class ValidationException extends Throwable {
+ public ValidationException(String message) {
+ super(message);
+ }
+}
diff --git a/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ExecutableTest.java b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ExecutableTest.java
new file mode 100644
index 000000000..dab8a5e17
--- /dev/null
+++ b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ExecutableTest.java
@@ -0,0 +1,102 @@
+package io.reflectoring.functional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.opentest4j.AssertionFailedError;
+
+public class ExecutableTest {
+ private final List numbers = Arrays.asList(100L, 200L, 50L, 300L);
+ final Executable sorter =
+ () -> {
+ TimeUnit.SECONDS.sleep(2);
+ numbers.sort(Long::compareTo);
+ };
+ private final Executable checkSorting =
+ () -> assertEquals(List.of(50L, 100L, 200L, 300L), numbers);
+ private final Executable noChanges = () -> assertEquals(List.of(100L, 200L, 50L, 300L), numbers);
+
+ @ParameterizedTest
+ @CsvSource({"1,1,2,Hello,H,bye,2,byebye", "4,5,9,Good,Go,Go,-10,", "10,21,31,Team,Tea,Stop,-2,"})
+ void testAssertAllWithExecutable(
+ int num1,
+ int num2,
+ int sum,
+ String input,
+ String prefix,
+ String arg,
+ int count,
+ String result) {
+ assertAll(
+ () -> assertEquals(sum, num1 + num2),
+ () -> assertTrue(input.startsWith(prefix)),
+ () -> {
+ if (count < 0) {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ new ArrayList<>(count);
+ });
+ } else {
+ assertEquals(result, arg.repeat(count));
+ }
+ });
+ }
+
+ @ParameterizedTest
+ @CsvSource({"one,0,o", "one,1,n"})
+ void testAssertDoesNotThrowWithExecutable(String input, int index, char result) {
+ assertDoesNotThrow(() -> assertEquals(input.charAt(index), result));
+ }
+
+ @Test
+ void testAssertThrowsWithExecutable() {
+ List input = Arrays.asList("one", "", "three", null, "five");
+ final IllegalArgumentException exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ for (String value : input) {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException("Got invalid value");
+ }
+ // process values
+ }
+ });
+ assertEquals("Got invalid value", exception.getMessage());
+ }
+
+ @Test
+ void testAssertTimeoutWithExecutable() {
+ assertAll(
+ () ->
+ assertThrows(
+ AssertionFailedError.class, () -> assertTimeout(Duration.ofSeconds(1), sorter)),
+ checkSorting);
+
+ assertAll(
+ () -> assertDoesNotThrow(() -> assertTimeout(Duration.ofSeconds(5), sorter)), checkSorting);
+ }
+
+ @Test
+ void testAssertTimeoutPreemptivelyWithExecutable() {
+ assertAll(
+ () ->
+ assertThrows(
+ AssertionFailedError.class,
+ () -> assertTimeoutPreemptively(Duration.ofSeconds(1), sorter)),
+ noChanges);
+
+ assertAll(
+ () -> assertDoesNotThrow(() -> assertTimeoutPreemptively(Duration.ofSeconds(5), sorter)),
+ checkSorting);
+ }
+}
diff --git a/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingConsumerTest.java b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingConsumerTest.java
new file mode 100644
index 000000000..b29cdc3f1
--- /dev/null
+++ b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingConsumerTest.java
@@ -0,0 +1,93 @@
+package io.reflectoring.functional;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.text.MessageFormat;
+import java.time.temporal.ValueRange;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+import org.junit.jupiter.api.function.ThrowingConsumer;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class ThrowingConsumerTest {
+ @ParameterizedTest
+ @CsvSource({"50,true", "130,false", "-30,false"})
+ void testMethodThatThrowsCheckedException(int percent, boolean valid) {
+ // acceptable percentage range: 0 - 100
+ ValueRange validPercentageRange = ValueRange.of(0, 100);
+ final Function message =
+ input ->
+ MessageFormat.format(
+ "Percentage {0} should be in range {1}", input, validPercentageRange.toString());
+
+ ThrowingConsumer consumer =
+ input -> {
+ if (!validPercentageRange.isValidValue(input)) {
+ throw new ValidationException(message.apply(input));
+ }
+ };
+
+ if (valid) {
+ assertDoesNotThrow(() -> consumer.accept(percent));
+ } else {
+ assertAll(
+ () -> {
+ ValidationException exception =
+ assertThrows(ValidationException.class, () -> consumer.accept(percent));
+ assertEquals(exception.getMessage(), message.apply(percent));
+ });
+ }
+ }
+
+ @TestFactory
+ Stream testDynamicTestsWithThrowingConsumer() {
+ // acceptable percentage range: 0 - 100
+ ValueRange validPercentageRange = ValueRange.of(0, 100);
+ final Function message =
+ input ->
+ MessageFormat.format(
+ "Percentage {0} should be in range {1}", input, validPercentageRange.toString());
+
+ // Define the ThrowingConsumer that validates the input percentage
+ ThrowingConsumer consumer =
+ testCase -> {
+ if (!validPercentageRange.isValidValue(testCase.percent)) {
+ throw new ValidationException(message.apply(testCase.percent));
+ }
+ };
+
+ ThrowingConsumer executable =
+ testCase -> {
+ if (testCase.valid) {
+ assertDoesNotThrow(() -> consumer.accept(testCase));
+ } else {
+ assertAll(
+ () -> {
+ ValidationException exception =
+ assertThrows(ValidationException.class, () -> consumer.accept(testCase));
+ assertEquals(exception.getMessage(), message.apply(testCase.percent));
+ });
+ }
+ };
+ // Test data: an array of test cases with inputs and their validity
+ Collection testCases =
+ Arrays.asList(new TestCase(50, true), new TestCase(130, false), new TestCase(-30, false));
+
+ Function displayNameGenerator =
+ testCase -> "Testing percentage: " + testCase.percent;
+
+ // Generate dynamic tests
+ return DynamicTest.stream(testCases.stream(), displayNameGenerator, executable);
+ }
+
+ // Helper record to represent a test case
+ record TestCase(int percent, boolean valid) {}
+}
diff --git a/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingSupplierTest.java b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingSupplierTest.java
new file mode 100644
index 000000000..8df716242
--- /dev/null
+++ b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingSupplierTest.java
@@ -0,0 +1,81 @@
+package io.reflectoring.functional;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTimeout;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.ThrowingSupplier;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.opentest4j.AssertionFailedError;
+
+public class ThrowingSupplierTest {
+ private final List numbers = Arrays.asList(100L, 200L, 50L, 300L);
+ private final Consumer> checkSorting =
+ list -> assertEquals(List.of(50L, 100L, 200L, 300L), list);
+
+ ThrowingSupplier> sorter =
+ () -> {
+ if (numbers == null || numbers.isEmpty() || numbers.contains(null)) {
+ throw new ValidationException("Invalid input");
+ }
+ TimeUnit.SECONDS.sleep(2);
+ return numbers.stream().sorted().toList();
+ };
+
+ @ParameterizedTest
+ @CsvSource({"25.0d,5.0d", "36.0d,6.0d", "49.0d,7.0d"})
+ void testDoesNotThrowWithSupplier(double input, double expected) {
+ ThrowingSupplier findSquareRoot =
+ () -> {
+ if (input < 0) {
+ throw new ValidationException("Invalid input");
+ }
+ return Math.sqrt(input);
+ };
+ assertEquals(expected, assertDoesNotThrow(findSquareRoot));
+ }
+
+ @Test
+ void testAssertTimeoutWithSupplier() {
+ // slow execution
+ assertThrows(AssertionFailedError.class, () -> assertTimeout(Duration.ofSeconds(1), sorter));
+
+ // fast execution
+ assertDoesNotThrow(
+ () -> {
+ List result = assertTimeout(Duration.ofSeconds(5), sorter);
+ checkSorting.accept(result);
+ });
+
+ // reset the number list and verify if the supplier validates it
+ Collections.fill(numbers, null);
+
+ ValidationException exception =
+ assertThrows(ValidationException.class, () -> assertTimeout(Duration.ofSeconds(1), sorter));
+ assertEquals("Invalid input", exception.getMessage());
+ }
+
+ @Test
+ void testAssertTimeoutPreemptivelyWithSupplier() {
+ // slow execution
+ assertThrows(
+ AssertionFailedError.class, () -> assertTimeoutPreemptively(Duration.ofSeconds(1), sorter));
+
+ // fast execution
+ assertDoesNotThrow(
+ () -> {
+ List result = assertTimeoutPreemptively(Duration.ofSeconds(5), sorter);
+ checkSorting.accept(result);
+ });
+ }
+}