使用Mockito通过反射模拟方法

时间:2013-04-12 11:57:26

标签: java reflection mockito

我们正在使用Mock-Factory为我们的开发人员提供关于模拟功能的最大舒适度,以及对mockito本身不太可能需要的专业知识。

为了做到这一点,我们的Mock-Factory提供了一个方法来创建一个模拟给定类名,方法名(通过regexp)和给定的返回值,它看起来如下(清除相关部分)对于这个问题):

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;
}

您可以看到方法名称与名称(regexp)和正确的给定返回类型匹配。

它工作正常,但我有点担心我必须构建人工参数数组params! 不,这个方法

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

没用! 但我真的不明白为什么!?

有人能告诉我上述代码的原因或更好的选择吗?

3 个答案:

答案 0 :(得分:14)

你不应该这样做。 Mockito是一个设计精良,易于学习,记录极为明确,几乎是事实上的标准框架。它是类型安全的,不需要反射,这使得测试易于阅读和理解。

让您的开发人员学习真正的Mockito并直接使用其API。他们会很乐意使用它,因为它会比你自己的超级api更好,更容易使用和更灵活的设计,并且他们会知道他们不会因为他们可能会使用它而无所学习Mockito在其他项目甚至其他工作中。

Mockito不需要其他专有API。因此,我建议的替代方案是忘记这一点,并向你的开发人员传授Mockito。

答案 1 :(得分:6)

那么你的方法并不是一个好的方法,它通常是过度工程的开发人员。即使你的团队是“幼崽”,也不像他们在使用Mockito时必须编写ASM。此外,如果你这样做,你可以避免Mockito提供的简单,表现力或可插拔性的所有好处。作为一名建筑师,我宁愿确保我的工程师了解他们正在做什么而不是把它们放在婴儿公园里。他们怎么能成为一个伟大的团队呢?

此处提供的实现可能过于简单化,无法支持处理反射,桥接方法,变量,覆盖等时可能遇到的所有情况。如果此代码失败,则无法理解消息。简而言之,您可以直接使用Mockito的所有好处,并且无论如何都不需要添加项目。

编辑:刚看到answer of JB Nizet,我完全赞同他。


但是为了回答你的问题,那里发生了什么。仔细看看你的代码,似乎你不想关心传递给方法的args。

假设您在被模拟的类中有以下实际方法:

String log2(String arg1, String arg2)

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

现在编译器看到了什么,第一个方法log2采用类型为String的2个参数,方法log1N采用2个参数,一个类型String和另一个类型String[](变量参数由编译器转换为数组)。

如果直接在这些方法上使用Mockito,您将编写以下内容。

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

您可以像普通的java一样编写logN("a", "b", "c", "d")。当你想使用参数匹配器时,你将使用2 arg方法编写它:

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

现在使用vararg方法:

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

在第一种情况下,Mockito足够聪明地理解最后两个参数匹配,必须进入最后一个vararg,即argn,所以如果只有3个参数,那么Mockito理解这个方法会匹配(varargs是flatened) 在第二种情况下,anyVararg表示对mockito,可能有任何参数计数。

现在,回到反射代码,Method.invoke的签名是:

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

传递真实参数时反射和变量的典型用法是:

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

或者这个invoke方法基于vararg,它可以这样写:

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

因此,在调用中传递的参数vararg数组必须实际匹配被调用方法的签名。

此调用当然会失败     log1N_method.invoke(mock,“a”,“b”,“c”,“d”);

因此,当您使用anyVararg尝试此行代码时,调用不符合被调用方法参数的签名:

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

只有方法m只有一个参数时,它才有效。然而,您必须将其转换为数组内部的反射API(因为vararg实际上是数组)。这里的诀窍是invoke(Object obj, Object... args)中的vararg与被调用的方法vararg混淆。

所以在我的例子中使用arg匹配器你应该这样做:

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

因此,如果只有一个参数是vararg,那就是同样的事情:

String log1(String... argn)

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

当然,你不能在非vararg方法上使用anyVararg,因为签名中的参数布局不匹配。

正如你在这里看到的那样,如果你采用这种方式将Mockito抽象给你的团队,你将不得不管理很多阶级奇怪的事情。我不是说这是不可能的。但作为此代码的所有者,您必须维护它,修复它,并考虑可能出错的事情,并使此抽象代码的用户理解它。

很抱歉感到如此咄咄逼人,这对我来说似乎是错的,我强调这些警告。

答案 2 :(得分:2)

我同意JB Nizet的观点,即您应该只允许开发人员使用本机API。

但是,如果您确实需要为正则表达式匹配的大量方法提供默认答案,并且您无法或者无法修复这意味着的超重界面,那么您可以使用this SO answer作为灵感来使用default Answers进行更安全的Mockito重构:

@Test public void yourTest() {
  YourClass yourClass = mock(YourClass.class, new DefaultAnswer("foo.*Bar", baz));
  when(yourClass.someOtherMethod()).thenReturn("Some custom result");
  /* test */
}

private class DefaultAnswer implements Answer<Object> {
  private final String methodRegex; // or save a Pattern object instead
  private final Object returnValue;

  DefaultAnswer(String methodRegex, Object returnValue) { /* set fields */ }

  @Override public Object answer(InvocationOnMock invocation) throws Throwable {
    if (invocation.getMethod().getName().matches(methodRegex)) {
      return returnValue;
    } else {
      return Mockito.RETURNS_DEFAULTS.answer(invocation);
    }
  }
}