Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Mockito to mock methods by reflection

We are using a Mock-Factory to give our developers the most possible comfort about mocking functionality with the less possible needed know-how about mockito itself.

To do so, our Mock-Factory is providing a method to create a mock given the class-name, the method-name (by regexp) and the given return value which looks about the following (cleand up to the relevant parts for this question):

public <T> T getMockForMethod(Class<T> clazz, String methodName, Object methodResponse)
{
  T mockForMethod = mock(clazz);
  for (Method m : clazz.getDeclaredMethods ())
  {
    if (m.getName ().matches (methodName) && 
        m.getReturnType ().isAssignableFrom (methodResponse.getClass ()))
    {
      try
      {
         Class<?>[] paramTypes = m.getParameterTypes ();
         Object[] params = new Object[paramTypes.length];
         for (Object o : params)
         {
           o = Mockito.anyObject ();
         }
         Mockito.when (m.invoke (mockForService, params)).thenReturn (methodResponse);
      }
      catch (IllegalArgumentException e)
      {
        e.printStackTrace (System.err);
      }
      catch (IllegalAccessException e)
      {
        e.printStackTrace (System.err);
      }
      catch (InvocationTargetException e)
      {
        e.printStackTrace (System.err);
      }
    }
  }
  return mockForMethod;
}

As u can see the method name is matched by name (regexp) and the correct given return type.

It works fine, but i'm a bit bothered about the fact that i have to build the artificial parameter-array params! And no, the approach

Mockito.when (m.invoke (mockForService, Mockito.anyVararg ())).thenReturn(methodResponse);

didn't work! But i don't really understand why!?

Can anyone give me the reason or a better alternative to the code above?

like image 950
Arno Jost Avatar asked Nov 28 '22 17:11

Arno Jost


2 Answers

You shouldn't do this. Mockito is a really well-designed, simple to learn, extremely well-documented, and almost de-facto standard framework. And it's type-safe and doesn't need reflection, which makes tests easy to read and understand.

Let your developers learn the real Mockito and use its API directly. They'll be happy to use it because it will have a better, easier to use and more flexible design than your own super-api, and they'll know that they won't learn Mockito for nothing because they'll probably use it in other projects or even other jobs.

Mockito doesn't need another proprietary API on top of it. My suggested alternative is thus to forget about this and teach Mockito to your developers.

like image 139
JB Nizet Avatar answered Dec 05 '22 03:12

JB Nizet


Well your approach isn't really a good one, its typically over-engineering developers candyland. Even if your team are "younglings" it's not like they have to write ASM when using Mockito. Plus if you go this way, you avoid all the benefit in simplicity, expressiveness or plugability that Mockito provides. As an architect I would rather make sure my engineers understand what they are doing rather than put them in a baby park. How can they become a great team otherwise ?

Also the implementation provided here is probably way too simplistic to support all the cases you can have when dealing with reflection, bridge methods, varargs, overriding, etc. It doesn't have understandable message if this piece of code fails. In short you loose all the benefit of using Mockito directly, and add unnecessary to the project anyway.

EDIT: Just saw the answer of JB Nizet, I agree completely with him.


But however for the sake of answering your question, what is happening with there. Given a short look at your code, it seems that you don't want to care about the args passed to the method.

So suppose you have the following real method in the class being mocked :

String log2(String arg1, String arg2)

and

String log1N(String arg1, String... argn)

Now what the compiler sees, a first method log2 that takes 2 parameter of type String and a method log1N that takes 2 parameter, one of type String and the other one of type String[] (variable arguments are transformed by the compiler to an array).

If using Mockito directly on those method you will write the following.

given(mock.log2("a", "b")).will(...);
given(mock.log1N("a", "b", "c", "d")).will(...);

You write logN("a", "b", "c", "d") just like plain java. And when you want to use argument matchers you will write this with the 2 arg method:

given(mock.log2(anyString(), anyString())).will(...);

And now with the vararg method :

given(mock.log1N(anyString(), anyString(), anyString())).will(...); // with standard arg matchers
given(mock.log1N(anyString(), Mockito.<String>anyVararg())).will(...); // with var arg matcher

In the first case Mockito is smart enough to understand that the last two argument matchers, must go in the last vararg, i.e. argn, so Mockito understand this method will matches if there is only 3 arguments (varargs being flatened) In the second case anyVararg indicates to mockito, there could be any count of arguments.

Now, going back to the reflection code, the signature of Method.invoke is :

public Object invoke(Object obj, Object... args)

Typical usage with reflection and varargs when passing real arguments would be :

log2_method.invoke(mock, "a", "b");
log1N_method.invoke(mock, "a", new String[] { "b", "c", "d" });

or as this invoke method is based on vararg it could be written like this :

log1N_method.invoke(mock, new Object[] {"a", new String[] { "b", "c", "d" }});

So the passed argument vararg array in invoke, must actually matches the signature of the called method.

This call will fail of course then log1N_method.invoke(mock, "a", "b", "c", "d");

So when you tried this line of code with anyVararg, the invocation wasn't respecting the signature of the called method arguments:

Mockito.when (m.invoke(mockForMethod, Mockito.anyVararg())).thenReturn(methodResponse);

It would only work if the method m had one argument only. And yet you would have to make it to the reflection API that's inside an array (because vararg are actually arrays). The trick here is that the vararg in invoke(Object obj, Object... args) is confusing with the called method vararg.

So using arg matchers with my example you should do that way :

when(
    log1N.invoke(mock, anyString(), new String[] { Mockito.<String>anyVararg() })
).thenReturn("yay");

So if there is only one argument that is a vararg, it's the same thing:

String log1(String... argn)

when(
    logN.invoke(mock, new String[] { Mockito.<String>anyVararg() })
).thenReturn("yay");

And of course you cannot use anyVararg on a non vararg method, because the argument layout in the signature won't match.

As you see here, if you go this way of abstracting Mockito to your team, you will have to manage a lot of class level oddities. I'm not saying this is impossible. But as an owner of this code you'll have to maintain it, fix it, and take into account may things that could go wrong, and make it understandable to the users of this abstraction code.

Sorry to feel so pushy, that seem so wrong to me that I stresses these warnings.

like image 41
Brice Avatar answered Dec 05 '22 04:12

Brice