Search code examples
javaspringunit-testinginstanceof

alternative to instanceof operator when specifying mocks for multiple web services


I am writing endpoint unit tests and for most of those there is an external web service that should be mocked, or a couple of them.

At first, i was creating mocks within tests which was okay when an endpoint test used only one external service, the mock creation was basically one liner.

As use cases became more complex, i needed to mock couple of services and exceptions for a single endpoint test. I have put these mocks creation behind factories that all extend single factory and used builder pattern.

Within that base factory there is an inner class which i used as a builder for MockWebServiceServer.

protected class MultiStepMockBuilder {

    private List<Object> mockActions = new ArrayList<Object>();
    private WebServiceGatewaySupport gatewaySupport;

    protected MultiStepMockBuilder(WebServiceGatewaySupport gatewaySupport) {

        this.gatewaySupport = gatewaySupport;
    }

    protected MultiStepMockBuilder exception(RuntimeException exception) {

        mockActions.add(exception);

        return this;

    }

    protected MultiStepMockBuilder resource(Resource resource) {

        mockActions.add(resource);

        return this;
    }

    protected MockWebServiceServer build() {

        MockWebServiceServer server =  MockWebServiceServer.createServer(gatewaySupport);

        for(Object mock: mockActions) {

            if (mock instanceof RuntimeException) {

                server.expect(anything()).andRespond(withException((RuntimeException)mock));
            } 

            else if (mock instanceof Resource)
            {

                try 
                {
                    server.expect(anything()).andRespond(withSoapEnvelope((Resource) mock));

                } catch (IOException e) {e.printStackTrace();}
            }

            else 
                throw new RuntimeException("unusuported mock action");
        }

        return server;
    }
  }
}

So i can now do something like this to create mock:

return new MultiStepMockBuilder(gatewaySupport).resource(success).exception(new WebServiceIOException("reserve timeout"))
                                               .resource(invalidMsisdn)
                                               .build();    

The issue i have with this implementation is dependence on instanceof operator which i never use outside of equals.

Is there an alternative way to instanceof operator in this scenario ? From the questions on topic of instanceof everybody argues it should only be used within equals and therefore i have feeling that this is 'dirty' solution.

Is there an alternative to instanceof operator, within Spring or as a different design, while keeping fluent interface for mocks creation ?


Solution

  • I don't know Spring well enough to comment specifically on this particular area, but to me, this just seems like a design thing. Generally, when you are faced with using instanceof, it means that you need to know the type, but you don't have the type. It is generally the case that we might need to refactor in order to achieve a more cohesive design that avoids this kind of problem.

    The root of where the type information is being lost, is in the List of mock actions, which are currently just being stored as a List of Objects. One way to help with this then, is to look at the type of the List and consider if there is a better type that could be stored in the List that might help us later. So we might end up with a refactoring something like this.

    private List<MockAction> mockActions = new ArrayList<MockAction>();
    

    Of course, then we have to decide what a MockAction actually is, as we've just made it up. Maybe something like this:

    interface MockAction {
      void performAction(MockWebServiceServer server);
    }
    

    So, we've just created this MockAction interface, and we've decided that instead of the caller performing the action - we're going to pass the server into it and ask the MockAction to perform itself. If we do this, then there will be no need for instanceof - because particular types of MockActions will know what they contain.

    So, what types of MockActions do we need?

    class ExceptionAction implements MockAction {
      private final Exception exception;
    
      private ExceptionAction(final Exception exception) {
        this.exception = exception;
      }
    
      public void performAction(final MockWebServiceServer server) {
        server.expect(anything()).andRespond(withException(exception);
      }
    
    }
    
    class ResourceAction implements MockAction {
    
      private final Resource resource;
    
      private ResourceAction(final Resource resource) {
        this.resource = resource;
      }
    
      public void performAction(final MockWebServiceServer server) {
        /* I've left out the exception handling */
        server.expect(anything()).andRespond(withSoapEnvelope(resource));
      }
    }
    

    Ok, so now we've gotten to this point, there are a couple of loose ends.

    We're still adding exceptions to the list of MockActions - but we need to change the add methods to make sure we put the right thing in the list. The new versions of these methods might look something like this:

    protected MultiStepMockBuilder exception(RuntimeException exception) {
    
        mockActions.add(new ExceptionAction(exception));
    
        return this;
    
    }
    
    protected MultiStepMockBuilder resource(Resource resource) {
    
        mockActions.add(new ResourceAction(resource));
    
        return this;
    }
    

    So, now we've left our interface the same, but we're wrapping the resource or exception as they're added to the list so that we have the type specificity we need later on.

    And then finally, we need to refactor our method that actually makes the calls, which now looks something like this - which is much simpler and cleaner.

    protected MockWebServiceServer build() {
        MockWebServiceServer server =  MockWebServiceServer.createServer(gatewaySupport);
    
        for(MockAction action: mockActions) {
            action.performAction(server);
        }
        return server;
    }