io.mats3.test.abstractunit.AbstractMatsTestEndpoint Maven / Gradle / Ivy
Show all versions of mats-test Show documentation
package io.mats3.test.abstractunit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.mats3.MatsEndpoint;
import io.mats3.MatsEndpoint.ProcessSingleLambda;
import io.mats3.MatsFactory;
/**
* Common base class which consolidates the common logic utilized by both Rule_MatsEndpoint and Extension_MatsEndpoint.
*
* - mats-test-junit
* - mats-test-jupiter
*
* Set's up a {@link MatsEndpoint} which processor can be modified on the fly. Also provides utility methods to extract
* incoming requests and verify that endpoint har or hasn't been invoked.
*
* @param
* The reply class of the message generated by this endpoint. (Reply Class)
* @param
* The incoming message class for this endpoint. (Request Class)
* @author Kevin Mc Tiernan, 2020-10-22, [email protected]
* @author Geir Gullestad Pettersen, 2017 - [email protected]
* @author Johan Herman Hausberg, 2017.04 - [email protected]
* @author Asbjørn Aarrestad, 2017 - [email protected]
* @author Endre Stølsvik, 2017 - http://stolsvik.com/, [email protected]
*/
public abstract class AbstractMatsTestEndpoint {
private static final Logger log = LoggerFactory.getLogger(AbstractMatsTestEndpoint.class);
/** Actual {@link MatsEndpoint} returned from the {@link MatsFactory} during creation. */
private MatsEndpoint _endpoint;
private final String _endpointId;
private final Class _replyMsgClass;
private final Class _incomingMsgClass;
protected MatsFactory _matsFactory;
protected volatile ProcessSingleLambda _processLambda;
/**
* Synchronized list keeping track of all endpoint invocations, storing the incoming message for later retrieval.
*/
private final SynchronizedInvocationList _synchronizedInvocationList = new SynchronizedInvocationList<>();
/**
* Base constructor for {@link AbstractMatsTestEndpoint}, takes all values needed to setup the test endpoint.
*
* @param endpointId
* Identifier of the endpoint being created.
* @param replyMsgClass
* Class of the reply message.
* @param incomingMsgClass
* Class of the incoming message. (Request)
*/
protected AbstractMatsTestEndpoint(String endpointId, Class replyMsgClass, Class incomingMsgClass) {
_endpointId = endpointId;
_replyMsgClass = replyMsgClass;
_incomingMsgClass = incomingMsgClass;
}
/**
* Set the {@link MatsFactory} of this class {@link #_matsFactory}. Shall be implemented by the extending class.
*
* @param matsFactory
* instance to store internally.
* @return this
for chaining.
*/
public abstract AbstractMatsTestEndpoint setMatsFactory(MatsFactory matsFactory);
/**
* Specify the processing lambda to be executed by the endpoint aka the endpoint logic. This is typically invoked
* either as part of the directly inside a test method to setup the behavior for that specific test or once through
* the initial setup when creating the test endpoint.
*
* @param processLambda
* which the endpoint should execute on an incoming request.
*/
public abstract AbstractMatsTestEndpoint setProcessLambda(ProcessSingleLambda processLambda);
/**
* Blocks and waits for the endpoint to be invoked, then returns the incoming message DTO of the type
* (I
). Will use a default timout value of 30 seconds.
*
* @return the first incoming message it encounters after calling this method.
*/
public I waitForRequest() {
return waitForRequest(30_000);
}
/**
* Blocks and waits for the endpoint to be invoked, then returns the incoming message DTO of the type
* (I
).
*
* @param millisToWait
* time to wait before timing out.
* @return the first incoming message it encounters after calling this method.
*/
public I waitForRequest(long millisToWait) {
return waitForRequests(1, millisToWait).get(0);
}
/**
* Blocks and waits for the endpoint to be invoked x number of times, then returns the x number of corresponding
* incoming message DTO's of the type (I
). Will utilize a default timeout value of 30 seconds.
*
* @param expectedNumberOfRequests
* the number of requests before unblocking and returning the received objects.
* @return the x number of incoming message it encounters after calling this method as a List<I>.
*/
public List waitForRequests(int expectedNumberOfRequests) {
return waitForRequests(expectedNumberOfRequests, 30_000);
}
/**
* Blocks and waits for the endpoint to be invoked x number of times, then returns the x number of corresponding
* incoming message DTO's of the type (I
).
*
* @param expectedNumberOfRequests
* the number of requests before unblocking and returning the received objects.
* @param millisToWait
* time to wait before timing out.
* @return the x number of incoming message it encounters after calling this method as a List<I>.
*/
public List waitForRequests(int expectedNumberOfRequests, long millisToWait) {
return _synchronizedInvocationList.getInvocationsWaitForCount(expectedNumberOfRequests, millisToWait);
}
/**
* Verifies that this endpoint has not been invoked. This can be useful in scenarios where an endpoint has multiple
* routes x,y and z. For example, given request a, the request should be processed and forwarded to y endpoint and
* this endpoint should not be invoked.
*
* @throws UnexpectedMatsTestEndpointInvocationError
* exception thrown if the endpoint has been invoked.
*/
public void verifyNotInvoked() {
// ?: Has there been any invocations of the endpoint?
if (_synchronizedInvocationList.hasInvocations()) {
// -> Yes, this was not expected as per invocation of this method. Throw hard.
throw new UnexpectedMatsTestEndpointInvocationError(_endpointId,
_synchronizedInvocationList.getNumberOfInvocations());
}
// E-> All good! Drift away...
}
// ======================== JUnit Lifecycle methods ===============================================================
private boolean isJunit() {
return getClass().getName().contains(".junit.");
}
private String junitOrJupiter() {
return isJunit() ? "JUnit" : "Jupiter";
}
protected String idThis() {
return this.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this));
}
/**
* Registers a {@link MatsEndpoint} with the provided {@link MatsFactory}, notice that the {@link MatsFactory} is
* not set or provided directly through this class through the use of the constructor or a method. It is up to the
* extending class to provide this factory.
*
* The created endpoint is created as a {@link MatsFactory#staged} endpoint, the reason behind this is that a staged
* endpoint does not require a return unlike a {@link MatsFactory#single}.
*
* This method should be called as a result of the following life cycle events for either JUnit or Jupiter:
*
* - Before - JUnit - Rule
* - BeforeEachCallback - Jupiter
*
*/
public void before() {
log.debug("+++ " + junitOrJupiter() + " +++ BEFORE on '" + idThis() + "'.");
// ?: Is the mats factory defined?
if (_matsFactory == null) {
// -> No, then we can't continue. Throw hard.
String testExecutionListener = isJunit()
? "SpringInjectRulesTestExecutionListener"
: "SpringInjectExtensionsTestExecutionListener";
throw new MatsFactoryNotSetException("== " + getClass().getSimpleName() + " == : MatsFactory is"
+ " not set, thus cannot complete setup of test endpoint.\n Provide me a MatsFactory either through"
+ " setting it explicitly through setMatsFactory(...), or if in a Spring testing context, annotate"
+ " your test class with '@SpringInjectRulesAndExtensions', or if that fails, "
+ " '@TestExecutionListeners(listeners = " + testExecutionListener
+ ".class, mergeMode = MergeMode.MERGE_WITH_DEFAULTS)' to inject it from the Spring Context.");
}
// Despite only using one stage, registering a multi-stage endpoint so that we can skip reply which is
// forced by a single stage endpoint. Thus, stateClass==void.class.
_endpoint = _matsFactory.staged(_endpointId, _replyMsgClass, void.class);
_endpoint.stage(_incomingMsgClass, (ctx, state, msg) -> {
try {
// ?: Has a processor been defined?
if (_processLambda != null) {
// -> Yes, execute it.
log.debug("+++ [" + _endpointId + "] executing user defined process lambda, incoming message"
+ " class:[" + (msg != null ? msg.getClass().getSimpleName() : "{null msg}")
+ "], expected reply class: [" + _replyMsgClass + "]");
ctx.reply(_processLambda.process(ctx, msg));
}
else {
// -> No, then we should not reply.
log.warn("+++ [" + _endpointId + "] no processor defined, thus not replying.");
}
}
finally {
// Utilizing a finally block to catch ALL invocations and ensure that the processor is executed.
// This ensures that any blocking waits in tests won't hold up the endpoint processor.
_synchronizedInvocationList.addInvocation(msg);
}
});
// For a multi stage, the invocation of lastStage() executes the finishSetup() however since we only utilize one
// stage with no last stage we need to explicitly call finishSetup() for the endpoint to start.
_endpoint.finishSetup();
log.debug("--- " + junitOrJupiter() + " --- /BEFORE done on '" + idThis() + "'.");
}
/**
* Shutdown and remove the endpoint from the {@link MatsFactory} after test and remove reference to endpoint from
* field.
*
* This method should be called as a result of the following life cycle events for either JUnit or Jupiter:
*
* - After - JUnit - Rule
* - AfterEachCallback - Jupiter
*
*/
public void after() {
log.debug("+++ " + junitOrJupiter() + " +++ AFTER on '" + idThis() + "'.");
Optional> endpoint = _matsFactory.getEndpoint(_endpointId);
endpoint.ifPresent(ep -> ep.remove(30_000));
_endpoint = null;
_synchronizedInvocationList.resetInvocations();
log.debug("--- " + junitOrJupiter() + " --- /AFTER done on '" + idThis() + "'.");
}
// =============== Util :: Synchronizer ===========================================================================
/**
* Util to keep a list of invocations and the ability to {@link Object#wait(long)} for a number of invocations with
* a timeout.
*
* Expected usage of class is multiple threads accessing {@link #addInvocation(Object)} and one test thread
* accessing either {@link #getInvocationsWaitForCount(int, long)} or {@link #hasInvocations()}.
*
* @param
* The class type of the object to be stored in the internal list.
*/
static class SynchronizedInvocationList {
private final Object _lock = new Object();
private final List _invocations = new ArrayList<>();
/**
* Blocks and waits for the {@link #_invocations invocations list} to contain the given count number of
* elements. Given that the list is filled up with the desired number of elements within the specified timeout
* period, it will return these elements. Should the number of elements not reach the specified count it will
* timeout and throw an {@link AssertionError}.
*
* @param count
* number of invocations to wait for.
* @param timeoutMillis
* milliseconds to wait before timing out.
* @return the number of count specified objects as a List<I>.
* @throws AssertionError
* if the expected number of invocations has not been received before timing out.
*/
List getInvocationsWaitForCount(int count, long timeoutMillis) {
synchronized (_lock) {
// StartTime representing when we entered this method.
long startTime = System.currentTimeMillis();
try {
// Loop until we have the desired number of invocations.
while (_invocations.size() < count) {
// How long since we first entered this method?
long elapsedTime = System.currentTimeMillis() - startTime;
// The reason we use remaining timeout is that we want to throw after timeout milliseconds and
// not count * timeout milliseconds. If count is 100, timeout is 5s and the code has become slow
// as hell (4s pr invocation), this method would otherwise not have thrown until 400s
long remainingTimeout = timeoutMillis - elapsedTime;
// Object::wait() doesn't throw when it times out (it only returns), so we need to check the
// timeout ourselves.
if (remainingTimeout <= 0) {
throw new AssertionError("+++ Expected [" + count + "] invocations, "
+ "but got [" + _invocations.size() + "] during the [" + timeoutMillis + "] timeout"
+ "window.");
}
// The other method will wake us up every time there is a new invocation
_lock.wait(remainingTimeout);
}
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
// :: When the loop exits we know that the desired number of invocations has been reached.
return _invocations;
}
}
void addInvocation(I invocation) {
synchronized (_lock) {
_invocations.add(invocation);
_lock.notifyAll();
}
}
void resetInvocations() {
synchronized (_lock) {
_invocations.clear();
_lock.notifyAll();
}
}
/**
* @return the number of elements in the internal {@link #_invocations invocations list}.
*/
int getNumberOfInvocations() {
synchronized (_lock) {
return _invocations.size();
}
}
/**
* @return true
if the underlying {@link #_invocations} list contains an entry, else
* false
.
*/
boolean hasInvocations() {
synchronized (_lock) {
return !_invocations.isEmpty();
}
}
}
// =========== Exceptions =========================================================================================
/**
* Thrown by {@link #verifyNotInvoked} should the internal {@link #_synchronizedInvocationList} contain any elements
* indicating an invocation.
*/
static final class UnexpectedMatsTestEndpointInvocationError extends AssertionError {
public UnexpectedMatsTestEndpointInvocationError(String endpointId, int numberOfInvocations) {
super("Unexpected invocation of MatsEndpoint with id[" + endpointId + "]. The endpoint was "
+ "invoked [" + numberOfInvocations + "] times.");
}
}
/**
* Thrown should the {@link #before()} be called without a {@link MatsFactory} be provided.
*/
static final class MatsFactoryNotSetException extends RuntimeException {
public MatsFactoryNotSetException(String msg) {
super(msg);
}
}
}