1  Basic Approach

To intercept, examine and optionally modify the Servlet API calls made to a particular mimic instance and the results of such calls, you need to access its mimicListeners property and install suitable listeners into it.

Each mimic instance’s mimicListeners property is a com.openbrace.obmimic.core.MimicListeners instance that can be accessed via the mimic’s getMimicListeners method. This has add methods through which com.openbrace.obcommon.lang.MethodInvocationListener and com.openbrace.obcommon.lang.MethodExitListener instances can be installed.

Whenever a call is made to any of the API methods that are being “mimicked” by the mimic, all MethodInvocationListeners within the mimic’s mimicListeners are notified when the API method is invoked, and all MethodExitListeners within the mimic’s mimicListeners are notified when the API method exits.

The details passed to each MethodInvocationListener include the mimic instance itself (as both the source of the event and the target object on which the API method is being called), the method being called, and the arguments being passed to the method. In addition to examining these details, the listener can also replace the arguments being passed to the method (or change the internal state of existing arguments where they are mutable). Any changes to the argument values will then be used both for the call itself and in any mimicHistory recording of the call. The listener can also abort the call by throwing any unchecked exception.

The details passed to each MethodExitListener include the mimic instance itself (as the source of the event), the details of the API call (as passed to each MethodInvocationListener when the call was invoked), whether the call completed successfully or failed with an exception, and either the exception thrown by the call or the value returned by the call (if any). In addition to examining these details, the listener can also replace the exception thrown by the call or the value returned by the call (or change the internal state of the returned value if this is mutable). This includes the ability to change a successful call to one that failed with an exception or vice-versa. Any changes made to the outcome of the call will then be used both for returning to the caller and in any mimicHistory recording of the call. The listener can also abort the corresponding return from the call by throwing an unchecked exception.

The following example uses some arbitrary example listeners to demonstrate how these facilities can be used to intercept, examine and modify various Servlet API method calls made to a ServletContextMimic during the processing of an HttpServletRequestMimic and HttpServletResponseMimic that use it as their servlet context:


  import com.openbrace.obmimic.mimic.servlet.http.HttpServletRequestMimic;
  import com.openbrace.obmimic.mimic.servlet.http.HttpServletResponseMimic;
  import com.openbrace.obmimic.mimic.servlet.ServletContextMimic;
  import com.openbrace.obcommon.lang.MethodInvocationListenerAdapter;
  import com.openbrace.obcommon.lang.MethodInvokedEvent;
  import com.openbrace.obcommon.lang.MethodInvocationDetails;
  import com.openbrace.obcommon.lang.MethodExitListenerAdapter;
  import com.openbrace.obcommon.lang.MethodExitedEvent;
  import com.openbrace.obcommon.lang.MethodResultDetails;
  import java.net.MalformedURLException;

  ...

  // Construct the request and response and their ServletContext.
  ServletContextMimic context = new ServletContextMimic();
  HttpServletRequestMimic request = new HttpServletRequestMimic();
  request.getMimicState().setServletContext(context);
  HttpServletResponseMimic response = new HttpServletResponseMimic();
  response.getMimicState().setServletContext(context);

  ...

  // Install a listener into the ServletContext that, as an example,
  // reports a failed test if the ServletContext's getResource method
  // is invoked with a path of "/" on its own.
  context.getMimicListeners().add(
      new MethodInvocationListenerAdapter() {
          @Override
          public void methodInvoked(final MethodInvokedEvent event) {
              super.methodInvoked(event);
              MethodInvocationDetails details
                = event.getMethodInvocationDetails();
              String methodName = details.getMethod().getName();
              if (methodName.equals("getResource")
                      && (details.getArgumentsCount() > 0)
                      && (details.getArgument(0) instanceof String)) {
                  String path = (String) details.getArgument(0);
                  if (path.equals("/")) {
                      // Report a failed test (as appropriate for
                      // whatever test framework is being used).
                      ...
                  }
              }
          }
      }
  );

  // Install a listener into the ServletContext that, as an example,
  // changes any call to the ServletContext's getAttribute method
  // with a name argument of null to pass it the name "x" instead.
  context.getMimicListeners().add(
      new MethodInvocationListenerAdapter() {
          @Override
          public void methodInvoked(final MethodInvokedEvent event) {
              super.methodInvoked(event);
              MethodInvocationDetails details
                  = event.getMethodInvocationDetails();
              String methodName = details.getMethod().getName();
              if (methodName.equals("getAttribute")
                      && (details.getArgumentsCount() > 0)
                      && (details.getArgument(0) == null)) {
                  details.setArgument(0, "x");
              }
          }
      }
  );

  // Install a listener into the ServletContext that, as an example,
  // changes the outcome of any call to the ServletContext's
  // getResource method that fails with a MalformedURLException so
  // that it instead completes successfully and returns null.
  context.getMimicListeners().add(
      new MethodExitListenerAdapter() {
          @Override
          public void methodExited(final MethodExitedEvent event) {
              super.methodExited(event);
              MethodResultDetails result = event.getMethodResultDetails();
              String methodName
                  = result.getInvocationDetails().getMethod().getName();
              if (methodName.equals("getResource")
                      && !result.wasSuccessful()
                      && (result.getThrownException()
                              instanceof MalformedURLException)) {
                  result.setSuccessful(null);
              }
          }
      }
  );

  // ...processing of the request and response for which Servlet API
  //    calls to the ServletContext's methods are to be notified to
  //    the listeners, including any relevant exception handling,
  //    checking of the results etc...

      

Note that:

  • The above example uses “Adapter” classes that are provided as convenient base classes for implementations of the MethodInvocationListener and MethodExitListener interfaces. For further details, see “Alternatives and Options” below.
  • The listeners installed into a mimic’s mimicListeners are invoked for all mimicked API methods on the instance, so each listener will typically need to identify the particular method calls or results that it needs to examine or modify, and ignore all other notifications. Typically this can be done by checking the name of the invoked method and details of the arguments passed to the method or the outcome of the call (as illustrated in the example below), but obviously this will depend on exactly what the listener is intended to do.
  • If multiple MethodInvocationListener instances are present for a mimic, they are notified in the order in which they were added into the mimic’s mimicListeners. This order may be significant if one or more of the listeners modify the method invocation’s arguments, as the details passed to each listener will include any changes that have been made by preceding listeners. Similarly, if multiple MethodExitListener instances are present they are notified in the order in which they were added into the mimic’s mimicListeners, and this order may be significant if one or more of the listeners modify the outcome of the method invocation.
  • Any exceptions thrown by listeners are propagated directly to the caller with no further processing of these listeners nor any mimicHistory recording. Specifically, if a MethodInvocationListener throws an exception, this is immediately thrown to the caller as if thrown by the invoked API method, with no further MethodInvocationListeners being notified, no invocation of the actual API method, and no recording of the call in the mimic’s mimicHistory. Similarly, if a MethodExitListener throws an exception, this is immediately thrown to the caller as if thrown by the invoked API method, with no further MethodExitListeners being notified, and no recording of the outcome of the call in the mimic’s mimicHistory (with any existing mimicHistory record for the call thus remaining in the “outcome not yet known” state).

Note, however, that these facilities are only available when using ObMimic with a valid “Professional” or “Enterprise” licence. The “Community” edition of ObMimic does not allow the use of the mimic instance’s getMimicListeners method and does not carry out any notification of Servlet API method calls and exits (with any attempt to access the getMimicListeners failing with an unchecked com.openbrace.obmimic.config.UnlicensedProFeatureException).

2  Alternatives and Options

In the above example, the listeners are constructed as anonymous inner classes that subclass default implementations provided by the MethodInvocationListenerAdapter and MethodExitListenerAdapter classes. These default implementations simply validate that the event arguments passed to them are non-null, with the example code using the inherited implementation to apply this validation. However, the use of these “Adapter” classes is in no way mandatory, and any implementations of the MethodInvocationListener and MethodExitListener interfaces are acceptable.

Similarly, although the above example uses two separate MethodInvocationListeners, another approach would be to combine these into a single listener that handles both types of call. However, in general it will usually be clearer and simpler to define and install a separate listener for each type of examination or modification that is required.

It is also possible to install the same listener instance into multiple mimic instances. Where necessary such listeners can use the events passed to them to determine which mimic instance they are being notified by, and can use that mimic’s normal facilities where necessary to examine its current state.

Note also that the MethodInvocationListener and MethodExitListener interfaces are both single-method interfaces, so if you are using Java™ SE 8 or higher (with support for “lambda” syntax) the code shown above can potentially be simplified somewhat.

As an alternative to using a mimic’s mimicListeners to intercept and examine or modify each Servlet API call to the mimic as it occurs, you can also use the mimic’s mimicHistory to record the details of a sequence of Servlet API calls to the mimic and then examine these as a whole afterwards. For details of the mimicHistory facilities and how they can be used, refer to the accompanying “How To” guide How to examine a history of the Servlet API calls made to a Mimic.

Depending on exactly what you are trying to do, you may find either the mimicHistory or the mimicListeners facilities more appropriate or easier to use (or may wish to use some combination of these to examine different details of the API calls). In particular, their main differences are:

  • The mimicHistory facilities allow you to examine the overall sequence of API calls to the mimic after completing all of a test’s API calls, whereas the mimicListeners facilities require you to process each API call’s details at the point where the call is actually being made.
  • The mimicHistory facilities capture immutable representations of each API call’s details and results as at the time of the call, whereas the details notified to listeners by the mimicListeners facilities are the actual objects being used as arguments and return values by the call. If any mutable arguments or return values passed to such listeners are captured and examined at some later point, their state may therefore have changed during or since the call.
  • The mimicHistory facilities can be used without the need for any custom classes, whereas use of the mimicListeners facilities requires appropriate listener implementations to be created.
  • The mimicHistory facilities can only record the details of each API call, but the mimicListeners facilities can also be used to modify the argument values used in the API call and the outcome of the call (that is, its returned value or thrown exception). For example, if the ObMimic implementation of an API method is considered unacceptable for any reason the mimicListeners facilities can be used to simulate different API behaviour.

3  Explanation and Further Details

As explained in the accompanying “How To” guide How to examine a history of the Servlet API calls made to a Mimic, ObMimic is primarily intended for state-based testing, where the result of a test is determined by examining its effect on the state of the objects involved, rather than interaction-based testing, where the result of a test is determined by examining what calls have been made internally between the objects involved.

Even where a more interaction-based test is required, the mimicHistory facilities will often be the most appropriate way to examine the API calls that have occurred during a test, as it captures an immutable record of each call, which can then be examined as a whole at the end of the test.

However, there may occasionally be circumstances where the ability to directly examine and potentially intervene in API calls and their results whilst they are occurring may be necessary or useful. The mimicListeners facilities have been introduced to enable this.

In particular, the ability to adjust an API call’s arguments before carrying out the call, or to adjust the result of an API call before returning it to the caller, provides a “last resort” work-around for any API method where ObMimic’s implementation is believed to be incorrect or some entirely non-standard API behaviour is required (for example, where the code you are testing internally uses a third-party framework that includes calls to the Servlet API that rely on incorrect or non-standard behaviour that ObMimic cannot otherwise be configured to simulate). At worst, one can modify the arguments to a call so that ObMimic regards the call as entirely valid, and then modify the outcome of the call to provide whatever result is required.

More generally, it may sometimes be simpler to install a listener for some particular type of API call and check its details or capture them for later checking, or to check that various calls occur in the correct order, compared to carrying out the same checks by examining a mimic’s mimicHistory. The provision of both facilities allows you to use whichever is most appropriate for each check that you need to carry out, and to mix and match the two approaches as you see fit.

To support the necessary listener facilities, every mimic instance has its own separate mimicListeners object into which listeners can be installed to be notified of relevant events. Methods are provided for installing and removing various types of listener, examining the listeners that are present, and notifying the relevant listeners of various events. At present this supports MethodInvocationListeners that are notified of each invocation of an API method, and MethodExitListeners that are notified of the completion/exit of each such API method call. Further types of listener may be added in future.

More specifically, the mimicListeners facility applies to exactly those API methods that are being “mimicked” by ObMimic. Conversely, it deliberately excludes any methods whose implementations are provided not by ObMimic but by an underlying default implementation supplied by the relevant API itself or by the underlying JDK, for which any calls directly invoke the underlying implementation with no intervention by ObMimic. This may, however, be reconsidered in future version of ObMimic if this proves to be inappropriate or confusing.

Note that although these mimicListeners facilities allow listeners to modify the arguments passed to mimicked API methods and the outcome of such calls:

  • It is recommended that the ability to modify method arguments and results is used sparingly and only as a “last resort”, and only when none of the other ObMimic configuration facilities can provide the required behaviour and you are certain that the required behaviour is correct and absolutely necessary. In particular, one of the aims of ObMimic is to allow you to test your code against ObMimic’s independently-implemented strict interpretation of the Servlet API javadoc, rather than just testing it against your own expectations about the Servlet API (which if incorrect will likely be incorrect in the same way in both the code being tested and your tests). Modifying the arguments and outcome of API calls whenever the ObMimic behaviour is not as you expect can easily undermine this.
  • The ability to modify the details of each API method invocation has been deliberately limited so that the method being invoked, the target object on which it is being invoked and the number of arguments being passed cannot be changed. This prevents the use of these listeners to invoke arbitrarily different API methods or to pass an invalid number of arguments to a method, whilst still allowing the individual arguments of each call to be changed.
  • Despite the aforementioned inability to change the number of arguments passed to a method, this does still support the use of “varargs” with a variable number of arguments, as such arguments are actually passed as a single argument whose value is an array containing the “varargs” argument’s variable number of arguments. It is thus possible to change the individual argument values within the “varargs” argument’s array, or to replace the “varargs” argument’s array with a different array that has a different number of individual argument values.