All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.mats3.test.abstractunit.AbstractMatsTestEndpoint Maven / Gradle / Ivy

Go to download

Mats^3 Testing tools, as well as the base for specific JUnit or Jupiter (Junit 5) tools.

The newest version!
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); } } }