reactor.test.publisher.PublisherProbe Maven / Gradle / Ivy
/*
* Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package reactor.test.publisher;
import java.util.concurrent.atomic.AtomicLongArray;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.publisher.TestPublisher.Violation;
/**
* A test utility that allow to easily obtain an instrumented {@link Publisher}
* ({@link Mono} or {@link Flux}) for tests involving control flow. For instance, you might
* have a {@link Mono#switchIfEmpty(Mono)} and you want to make sure that your code
* branched into the "if empty" case. The contract of this interface does not cover what
* signals the {@link Publisher} emits, although factory methods {@link #of(Publisher)}
* and {@link #empty()} produce probes that do emit signals like a common sequence.
*
* The {@link PublisherProbe} acts as a probe capturing subscription, cancellation and
* request events. Later, it can be used post completion to check if that particular
* probe was hit.
*
* Even though {@link TestPublisher} implements {@link PublisherProbe}, prefer creating
* probes through the static {@link #empty()} and {@link #of(Publisher)} methods.
* This is because the {@link TestPublisher} exposes assertions from {@link PublisherProbe}
* but still requires you to 1) use the {@link TestPublisher#emit(Object[]) TestPublisher emit methods}
* and 2) use the {@link Violation#CLEANUP_ON_TERMINATE} in order for these assertions to
* be usable post-completion...
*
* @author Simon Baslé
*/
public interface PublisherProbe {
/**
* Check that the probe was never subscribed to, or throw an {@link AssertionError}.
*/
default void assertWasNotSubscribed() {
if (wasSubscribed()) {
throw new AssertionError("PublisherProbe should not have been subscribed but it was");
}
}
/**
* Check that the probe was subscribed to at least once, or throw an {@link AssertionError}.
*/
default void assertWasSubscribed() {
if (!wasSubscribed()) {
throw new AssertionError("PublisherProbe should have been subscribed but it wasn't");
}
}
/**
* Check that the probe was never cancelled, or throw an {@link AssertionError}.
*/
default void assertWasNotCancelled() {
if (wasCancelled()) {
throw new AssertionError("PublisherProbe should not have been cancelled but it was");
}
}
/**
* Check that the probe was cancelled at least once, or throw an {@link AssertionError}.
*/
default void assertWasCancelled() {
if (!wasCancelled()) {
throw new AssertionError("PublisherProbe should have been cancelled but it wasn't");
}
}
/**
* Check that the probe was never requested, or throw an {@link AssertionError}.
*/
default void assertWasNotRequested() {
if (wasRequested()) {
throw new AssertionError("PublisherProbe should not have been requested but it was");
}
}
/**
* Check that the probe was requested at least once, or throw an {@link AssertionError}.
*/
default void assertWasRequested() {
if (!wasRequested()) {
throw new AssertionError("PublisherProbe should have been requested but it wasn't");
}
}
/**
* Return a {@link Mono} version of the probe. Note all calls to mono() and
* {@link #flux()} are backed by the same {@link PublisherProbe} and as such influence
* a single state.
*
* If the probe was {@link #of(Publisher) created out of a Publisher}, the {@link Flux}
* will forward the signals from this publisher (up to one {@link Subscriber#onNext(Object)} though).
* {@link #empty() Otherwise} it will simply complete.
*
* @return a {@link Mono} version of the probe.
*/
Mono mono();
/**
* Return a {@link Flux} version of the probe. Note all calls to {@link #mono()} and
* flux() are backed by the same {@link PublisherProbe} and as such influence
* a single state.
*
* If the probe was {@link #of(Publisher) created out of a Publisher}, the {@link Flux}
* will forward the signals from this publisher
* {@link #empty() Otherwise} it will simply complete.
*
* @return a {@link Flux} version of the probe.
*/
Flux flux();
/**
* @return true if the probe was subscribed to at least once.
*/
boolean wasSubscribed();
/**
* @return how many times probe was subscribed
*/
long subscribeCount();
/**
* @return true if the probe was cancelled to at least once.
*/
boolean wasCancelled();
/**
* @return true if the probe was requested at least once.
*/
boolean wasRequested();
/**
* Create a {@link PublisherProbe} out of a {@link Publisher}, ensuring that its
* {@link #flux()} and {@link #mono()} versions will propagate signals from this
* publisher while capturing subscription, cancellation and request events around it.
*
* @param source the source publisher to mimic and probe.
* @param the type of the source publisher.
* @return a probe that mimics the publisher.
*/
static PublisherProbe of(Publisher extends T> source) {
return new DefaultPublisherProbe<>(source);
}
/**
* Create a {@link PublisherProbe} of which {@link #flux()} and {@link #mono()}
* versions will simply complete, capturing subscription, cancellation and request
* events around them.
*
* @param the type of the empty probe.
* @return a probe that mimics an empty publisher.
*/
static PublisherProbe empty() {
return new DefaultPublisherProbe<>(Mono.empty());
}
final class DefaultPublisherProbe
extends AtomicLongArray
implements PublisherProbe {
private static final int SUBSCRIBED = 0;
private static final int CANCELLED = 1;
private static final int REQUESTED = 2;
final Publisher delegate;
@SuppressWarnings("unchecked")
DefaultPublisherProbe(Publisher extends T> delegate) {
super(3);
this.delegate = (Publisher) delegate;
}
@Override
public Mono mono() {
return Mono.from(delegate)
.doOnSubscribe(sub -> incrementAndGet(SUBSCRIBED))
.doOnCancel(() -> incrementAndGet(CANCELLED))
.doOnRequest(l -> incrementAndGet(REQUESTED));
}
@Override
public Flux flux() {
return Flux.from(delegate)
.doOnSubscribe(sub -> incrementAndGet(SUBSCRIBED))
.doOnCancel(() -> incrementAndGet(CANCELLED))
.doOnRequest(l -> incrementAndGet(REQUESTED));
}
@Override
public boolean wasSubscribed() {
return get(SUBSCRIBED) > 0;
}
@Override
public long subscribeCount() {
return get(SUBSCRIBED);
}
@Override
public boolean wasCancelled() {
return get(CANCELLED) > 0;
}
@Override
public boolean wasRequested() {
return get(REQUESTED) > 0;
}
}
}