Skip to content

Commit 857aab8

Browse files
jakubmalekJakub Malek
authored andcommitted
Refactoring
1 parent ba63a4d commit 857aab8

24 files changed

+1196
-603
lines changed

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,34 @@
1-
# assertj-async
1+
# AssertJ - Async
2+
23
AssertJ extension for making asynchronous assertions.
4+
5+
## Usage
6+
7+
The entry point for making asynchronous assertion is `AsyncAssertions` class.
8+
9+
Here is an example of test:
10+
```java
11+
class AsyncAssertionExampleTest
12+
{
13+
@Test
14+
void shouldReceiveChangeMessages() {
15+
// given
16+
var receivedMessages = new ConcurrentLinkedQueue<String>();
17+
18+
// when
19+
listenToChanges(receivedMessages::offer);
20+
21+
// then
22+
awaitAtMostOneSecond().untilAssertions(async -> async
23+
.assertThat(receivedMessages).containsExactly("A", "B", "C"));
24+
}
25+
26+
private static void listenToChanges(Consumer<String> consumer) {
27+
// simulation of asynchronous consumer
28+
var scheduler = Executors.newSingleThreadScheduledExecutor();
29+
scheduler.schedule(() -> consumer.accept("A"), 100L, TimeUnit.MILLISECONDS);
30+
scheduler.schedule(() -> consumer.accept("B"), 200L, TimeUnit.MILLISECONDS);
31+
scheduler.schedule(() -> consumer.accept("C"), 300L, TimeUnit.MILLISECONDS);
32+
}
33+
}
34+
```

build.gradle.kts

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1+
import nebula.plugin.contacts.Contact
2+
13
plugins {
4+
id("org.owasp.dependencycheck") version "7.1.1"
5+
id("nebula.release") version "16.0.0"
6+
id("nebula.maven-nebula-publish") version "18.4.0"
7+
id("nebula.maven-developer") version "18.4.0"
8+
id("nebula.maven-scm") version "18.4.0"
9+
id("nebula.contacts") version "6.0.0"
10+
id("nebula.info-scm") version "11.3.3"
11+
id("tylerthrailkill.nebula-mit-license") version "0.0.3"
12+
213
jacoco
314
`java-library`
415
`maven-publish`
516
`project-report`
617
}
718

819
java {
20+
sourceCompatibility = JavaVersion.VERSION_11
921
targetCompatibility = JavaVersion.VERSION_11
1022
}
1123

@@ -16,9 +28,9 @@ repositories {
1628
dependencies {
1729
// api
1830
api("org.assertj", "assertj-core", "[3.23.0,3.24.0[")
19-
api("org.opentest4j", "opentest4j", "[1.2.0,1.3.0[")
31+
compileOnly("org.opentest4j", "opentest4j", "[1.2.0,1.3.0[")
2032

21-
// Lombok
33+
// lombok
2234
compileOnly(annotationProcessor("org.projectlombok", "lombok", "[1.18.0,2.0.0["))
2335
testCompileOnly(testAnnotationProcessor("org.projectlombok", "lombok", "[1.18.0,2.0.0["))
2436

@@ -33,32 +45,10 @@ tasks {
3345
}
3446
}
3547

36-
publishing {
37-
publications {
38-
create<MavenPublication>("mavenJava") {
39-
pom {
40-
description.set("AssertJ extension for making asynchronous assertions")
41-
url.set("https://github.com/Webfleet-Solutions/assertj-async")
42-
licenses {
43-
license {
44-
name.set("The MIT License")
45-
url.set("https://github.com/Webfleet-Solutions/assertj-async/blob/main/LICENSE")
46-
}
47-
}
48-
developers {
49-
developer {
50-
id.set("jakubmalek")
51-
name.set("Jakub Malek")
52-
email.set("[email protected]")
53-
}
54-
}
55-
scm {
56-
url.set("https://github.com/Webfleet-Solutions/assertj-async")
57-
}
58-
issueManagement {
59-
url.set("https://github.com/Webfleet-Solutions/assertj-async/issues")
60-
}
61-
}
62-
}
63-
}
48+
contacts {
49+
addPerson("[email protected]", delegateClosureOf<Contact> {
50+
github = "jakubmalek"
51+
moniker = "Jakub Małek"
52+
role("owner")
53+
})
6454
}

gradle.properties

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
version=1.0
2-
build=SNAPSHOT
3-
title=assertj-async
4-
group=com.webfleet
1+
group=com.webfleet
2+
release.scope=major
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.webfleet.assertj;
2+
3+
import java.time.Duration;
4+
import java.util.concurrent.TimeUnit;
5+
import java.util.function.Consumer;
6+
7+
import org.assertj.core.api.SoftAssertions;
8+
9+
import lombok.NonNull;
10+
11+
12+
/**
13+
* Asynchronous assertion.
14+
*/
15+
public interface AsyncAssert
16+
{
17+
/**
18+
* Awaits until all assertions are passed or timeout is exceeded.
19+
* Assertions are configured in lambda consumer of {@link SoftAssertions} object on each check.
20+
* The checks are executed periodically with check interval delay configured with {@link AsyncAssert#withWaitInterval} method.
21+
* After exceeding timeout {@link AssertionError} will be thrown with failures from last assertion check.
22+
*
23+
* <pre><code class='java'>
24+
* awaitAtMostOneSecond().untilAssertions(async -> {
25+
* async.assertThat(getValue()).isEqualTo(expected);
26+
* async.assertThat(isDone()).isTrue();
27+
* });
28+
* </code></pre>
29+
*
30+
* @param assertionsConfiguerer lambda consumer configuring {@link SoftAssertions} object
31+
*/
32+
void untilAssertions(Consumer<SoftAssertions> assertionsConfiguerer);
33+
34+
/**
35+
* Configures assertion to use give mutex object for check interval wait logic.
36+
*
37+
* In multi-threaded applications, the mutex object can be used to notify the other thread about state change.
38+
* For asynchronous assertion, the mutex object can be used to reduce the wait time between checks with {@link Object#notifyAll()} call.
39+
*
40+
* @param waitMutex mutex object
41+
* @return new {@link AsyncAssert} using given wait mutex
42+
*/
43+
AsyncAssert usingWaitMutex(Object waitMutex);
44+
45+
/**
46+
* Configures the interval to be waited between assertions checks.
47+
* The interval must be greater than zero and lower than timeout.
48+
*
49+
* @param checkInterval check interval
50+
* @return new {@link AsyncAssert} with set check interval
51+
*/
52+
AsyncAssert withCheckInterval(Duration checkInterval);
53+
54+
/**
55+
* Configures the interval to be waited between assertions checks.
56+
* The interval must be greater than zero and lower than timeout.
57+
*
58+
* @param checkInterval check interval
59+
* @param unit the time unit of the check interval
60+
* @return new {@link AsyncAssert} with set check interval
61+
*/
62+
default AsyncAssert withWaitInterval(final long checkInterval, @NonNull final TimeUnit unit)
63+
{
64+
return withCheckInterval(Duration.ofMillis(unit.toMillis(checkInterval)));
65+
}
66+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.webfleet.assertj;
2+
3+
import java.time.Duration;
4+
import java.util.function.Consumer;
5+
6+
import org.assertj.core.api.SoftAssertions;
7+
8+
import lombok.AccessLevel;
9+
import lombok.AllArgsConstructor;
10+
import lombok.NonNull;
11+
12+
13+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
14+
final class AsyncAssertImpl implements AsyncAssert
15+
{
16+
private final Time time;
17+
private final AsyncAssertTimeoutCondition timeCondition;
18+
private final Object waitMutex;
19+
20+
AsyncAssertImpl(@NonNull final Time time, @NonNull final AsyncAssertTimeoutCondition timeCondition)
21+
{
22+
this(time, timeCondition, new Object());
23+
}
24+
25+
@Override
26+
public AsyncAssert withCheckInterval(@NonNull final Duration checkInterval)
27+
{
28+
return new AsyncAssertImpl(time, timeCondition.withCheckInterval(checkInterval), waitMutex);
29+
}
30+
31+
@Override
32+
public AsyncAssert usingWaitMutex(@NonNull final Object waitMutex)
33+
{
34+
return new AsyncAssertImpl(time, timeCondition, waitMutex);
35+
}
36+
37+
@Override
38+
public void untilAssertions(@NonNull final Consumer<SoftAssertions> assertionsConfigurer)
39+
{
40+
final var elapsedTime = time.measure();
41+
final var waitCondition = time.waitCondition(waitMutex);
42+
43+
var result = AsyncAssertResult.undefined();
44+
while (result.hasFailed() && elapsedTime.isLowerThanOrEqualTo(timeCondition.timeout()) && !Thread.currentThread().isInterrupted())
45+
{
46+
result = AsyncAssertResult.evaluate(assertionsConfigurer);
47+
if (result.hasFailed())
48+
{
49+
if (!elapsedTime.isLowerThan(timeCondition.timeout()))
50+
{
51+
break;
52+
}
53+
waitCondition.waitFor(timeCondition.checkInterval(elapsedTime));
54+
}
55+
}
56+
result.throwOnFailure(timeCondition);
57+
}
58+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.webfleet.assertj;
2+
3+
import static org.assertj.core.api.Assertions.catchThrowableOfType;
4+
5+
import java.util.function.Consumer;
6+
7+
import org.assertj.core.api.SoftAssertions;
8+
9+
import lombok.AccessLevel;
10+
import lombok.AllArgsConstructor;
11+
import lombok.NonNull;
12+
13+
14+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
15+
final class AsyncAssertResult
16+
{
17+
private static final AssertionError UNDEFINED_ERROR = new AssertionError("Failed to evaluate async assertions");
18+
19+
private final AssertionError error;
20+
21+
static AsyncAssertResult undefined()
22+
{
23+
return new AsyncAssertResult(UNDEFINED_ERROR);
24+
}
25+
26+
static AsyncAssertResult evaluate(@NonNull final Consumer<SoftAssertions> assertionConfigurer)
27+
{
28+
final var assertions = new SoftAssertions();
29+
// catching error in case assertAll is called explicitly by the consumer
30+
final var caughtError = catchThrowableOfType(() -> assertionConfigurer.accept(assertions), AssertionError.class);
31+
if (caughtError != null)
32+
{
33+
return new AsyncAssertResult(caughtError);
34+
}
35+
return new AsyncAssertResult(catchThrowableOfType(assertions::assertAll, AssertionError.class));
36+
}
37+
38+
boolean hasFailed()
39+
{
40+
return error != null;
41+
}
42+
43+
void throwOnFailure(@NonNull final AsyncAssertTimeoutCondition timeCondition)
44+
{
45+
if (hasFailed())
46+
{
47+
throw AsyncAssertionErrorCreator.create(timeCondition, error);
48+
}
49+
}
50+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.webfleet.assertj;
2+
3+
import static java.time.Duration.ZERO;
4+
5+
import java.time.Duration;
6+
7+
import com.webfleet.assertj.Time.ElapsedTime;
8+
9+
import lombok.AccessLevel;
10+
import lombok.AllArgsConstructor;
11+
import lombok.EqualsAndHashCode;
12+
import lombok.Getter;
13+
import lombok.NonNull;
14+
import lombok.ToString;
15+
import lombok.experimental.Accessors;
16+
17+
18+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
19+
@Accessors(fluent = true)
20+
@Getter
21+
@EqualsAndHashCode
22+
@ToString
23+
final class AsyncAssertTimeoutCondition
24+
{
25+
private static final Duration DEFAULT_CHECK_INTERVAL = Duration.ofMillis(100L);
26+
private static final Duration DEFAULT_SHORT_CHECK_INTERVAL = Duration.ofMillis(50L);
27+
28+
private final Duration timeout;
29+
private final Duration checkInterval;
30+
31+
static AsyncAssertTimeoutCondition withTimeout(@NonNull final Duration timeout)
32+
{
33+
if (timeout.compareTo(ZERO) <= 0)
34+
{
35+
throw new IllegalArgumentException("timeout must be greater than zero");
36+
}
37+
final var checkInterval = computeCheckInterval(timeout);
38+
return new AsyncAssertTimeoutCondition(timeout, checkInterval);
39+
}
40+
41+
AsyncAssertTimeoutCondition withCheckInterval(@NonNull final Duration checkInterval)
42+
{
43+
if (checkInterval.compareTo(ZERO) <= 0)
44+
{
45+
throw new IllegalArgumentException("checkInterval must be greater than zero");
46+
}
47+
if (checkInterval.compareTo(timeout) > 0)
48+
{
49+
throw new IllegalArgumentException("checkInterval must be lower than or equal to timeout");
50+
}
51+
return new AsyncAssertTimeoutCondition(timeout, checkInterval);
52+
}
53+
54+
Duration checkInterval(@NonNull final ElapsedTime elapsedTime)
55+
{
56+
final var elaspsedDuration = elapsedTime.get();
57+
if (elaspsedDuration.plus(checkInterval).compareTo(timeout) > 0)
58+
{
59+
final var shortenedCheckInterval = timeout.minus(elaspsedDuration);
60+
return shortenedCheckInterval.isNegative() ? Duration.ZERO : shortenedCheckInterval;
61+
}
62+
return checkInterval;
63+
}
64+
65+
private static Duration computeCheckInterval(final Duration timeout)
66+
{
67+
if (DEFAULT_CHECK_INTERVAL.compareTo(timeout) >= 0)
68+
{
69+
if (timeout.compareTo(DEFAULT_SHORT_CHECK_INTERVAL) > 0)
70+
{
71+
return DEFAULT_SHORT_CHECK_INTERVAL;
72+
}
73+
return timeout;
74+
}
75+
return DEFAULT_CHECK_INTERVAL;
76+
}
77+
}

0 commit comments

Comments
 (0)