Compact mockito: shorter answer notation

[Update 2014.10.08] The code is now on github.

If you value brevity in tests like I do then maybe you would agree that mockito’s doAnswer statements contain much boilerplate. That is certainly not mockito’s fault but rather a result of java’s inflexible syntax. When testing, i.e. GWT code one will frequently find the need to mock RPC service interfaces:

Order someOrder = …

doAnswer(new Answer(){

  @Override
  public Object answer(InvocationOnMock invocation) throws Throwable {
    AsyncCallback callback = (AsyncCallback)(invocation.getArguments()[2]);
    callback.onSuccess(someOrder);
    return null;
 }
}).when(orderService).getOrderForCustomer(eq(123), eq(456), any(AsyncCallback.class));

By using the BaseAnswer class discusses here you can achieve much shorter statements:

Order someOrder = ...

doAnswer(new BaseAnswer(){   @Override
  public void getOrderForCustomer(int customerId, int orderId, AsyncCallback callback) {
    callback.onSuccess(someOrder);
    }
 }).when(orderService).getOrderForCustomer(eq(123), eq(456), any(AsyncCallback.class));
And this is the BaseAnswer class:
import java.lang.reflect.Method;

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.util.ReflectionUtils;

public abstract class BaseAnswer implements Answer{

    private Class[] getClasses(Object[] arguments){
        Class[] argClasses = new Class[arguments.length];
        for (int i=0;i<arguments.length;i++){
            if (arguments[i]!=null)
                argClasses[i] = arguments[i].getClass();
        }
        return argClasses;
    }
   
    private double rateMatch(Class[] providedClasses, Class[] declaredClasses){
        double score = -1;
        if (providedClasses.length!=declaredClasses.length)
            return -1;
        for (int i=0;i<providedClasses.length;i++){
            if (providedClasses[i] == null)
                score+=0.5;
            else{
                if (!declaredClasses[i].isAssignableFrom(providedClasses[i]))
                    return -1;
                score+=1;
            }
        }
        score = score/(double)providedClasses.length;
        return score;
    }
   
    private Method findMethodWithArguments(Object[] arguments){
        Class[] argClasses = getClasses(arguments);
        Method[] methods = getClass().getDeclaredMethods();
        Method bestMethod = null;
        double bestMatch = -1;
        for (Method method:methods){
            double match = rateMatch(argClasses, method.getParameterTypes());
            if (match>bestMatch){
                bestMatch = match;
                bestMethod = method;
            }
        }
        return bestMethod;
    }
   
    private String argTypesToString(Object args[]){
        Class[] classes = getClasses(args);
        String s = "";
        String prefix=",";
        for (Class c:classes){
            s+=prefix+c.getName();
            prefix=",";
        }
        return s;
    }
   
    @SuppressWarnings("unchecked")
    @Override
    public T answer(InvocationOnMock invocation) throws Throwable {
        Object[] arguments = invocation.getArguments();
        Method method = findMethodWithArguments(arguments);
        if (method == null)
            throw new RuntimeException("This answer does not declare a method with these argument types: "+argTypesToString(arguments));
        ReflectionUtils.makeAccessible(method);
        return (T)method.invoke(this, arguments);
    }
} 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.