使用Mockito通过反射模拟方法

4
我们使用Mock-Factory来为开发人员提供最大的舒适性,以实现对mockito本身所需的最小知识。为此,我们的Mock-Factory提供了一种方法来创建一个mock对象,需要提供类名、方法名(通过正则表达式)和给定的返回值,该方法看起来大致如下(仅保留与本问题相关的部分):
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;
}

正如您所看到的,该方法名是通过名称(正则表达式)和正确的返回类型进行匹配。

它可以正常工作,但我有点困扰的是我必须构建人工参数数组params!而且,这种方法并不是最佳选择。

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

没能生效!但是我真的不明白为什么!?

有人可以告诉我原因或者更好的替代代码吗?


3
我的天啊!这就像是买了一辆法拉利,然后坚持用自行车拖着它行驶。请不要这样做。 - Dawood ibn Kareem
这个Mockito.when(m.invoke(mockForService, Mockito.anyVararg())).thenReturn(methodResponse)对你有用吗?因为我看到m.invoke正在调用方法并抛出异常。你能否请看一下我的问题?https://stackoverflow.com/questions/65592904/how-to-mock-methods-by-reflection-using-mockito - uzzi
3个回答

16

你不应该这样做。Mockito 是一个设计优秀、易于学习、文档极佳的、几乎成为事实标准的框架。它是类型安全的,不需要反射,使得测试容易阅读和理解。

让你的开发人员学习真正的 Mockito 并直接使用其 API。他们会很高兴使用它,因为它比你自己的超级 API 更好、更易于使用和更加灵活,并且他们会知道他们学习 Mockito 不是白费力气,因为他们可能在其他项目甚至其他工作中使用它。

Mockito 不需要另外一个专有的 API。因此,我建议你忘记这个并教你的开发人员如何使用 Mockito。


你怎么能说Mockito不需要反射呢? Mockito在内部使用反射。你可以在Mockito Git库中查看这个源代码。 - saran3h

8
你的方法并不是一个好方法,它通常会过度工程化开发人员的乐园。即使你的团队是“年轻人”,使用Mockito时也不必写ASM。此外,如果你采用这种方式,你就会失去Mockito提供的简单性、表现力或可插拔性的所有好处。作为一名架构师,我更愿意确保我的工程师们理解他们正在做什么,而不是把他们放在一个儿童公园里。否则,他们怎么能成为一个优秀的团队呢?
此外,这里提供的实现可能过于简单,无法支持处理反射、桥接方法、varargs、重写等所有情况。如果这段代码失败,它没有可理解的消息。简而言之,你失去了直接使用Mockito的所有好处,并且在项目中增加了不必要的复杂性。
编辑:刚看到JB Nizet的答案,我完全同意他的观点。
但是出于回答你的问题的目的,我会为你翻译与编程有关的内容。看了一下你的代码,似乎你不关心传递给方法的参数。假设你在被模拟类中有以下真实方法:
String log2(String arg1, String arg2)

并且

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

现在编译器看到的是一个名为log2的方法,它有两个参数,类型均为String,还有一个名为log1N的方法,它有两个参数,其中一个是String类型,另一个是String[]类型(可变参数由编译器转换为数组)。
如果直接在这些方法上使用Mockito,您将编写以下内容。
given(mock.log2("a", "b")).will(...);
given(mock.log1N("a", "b", "c", "d")).will(...);

你可以像普通的Java一样编写logN("a", "b", "c", "d")。当你想要使用参数匹配器时,你需要使用这个两个参数的方法来编写:
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足够聪明,可以理解最后两个参数匹配器必须放在最后一个可变参数中,即argn,因此Mockito理解该方法仅在有3个参数(可变参数被展开)时匹配。
在第二种情况下,anyVararg表示对Mockito来说可能有任意数量的参数。
现在回到反射代码,Method.invoke的签名是:
public Object invoke(Object obj, Object... args)

使用反射和varargs传递实际参数的典型用法如下:

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

或者,由于这个调用方法是基于可变参数的,它可以写成这样:

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

因此,在调用invoke方法时,传递的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混淆。
因此,使用我的示例中的参数匹配器,你应该这样做:
when(
    log1N.invoke(mock, anyString(), new String[] { Mockito.<String>anyVararg() })
).thenReturn("yay");

所以如果只有一个变长参数作为参数,那么这个和之前说的是一样的:

String log1(String... argn)

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

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

正如您在这里看到的,如果您采用这种将Mockito抽象化给您的团队的方式,您将不得不管理许多类级别的奇怪问题。我并不是说这是不可能的。但作为这段代码的所有者,您必须维护它,修复它,并考虑可能出错的许多事情,并使其对这个“抽象”代码的用户易于理解。

很抱歉感到有些强迫,我觉得这样做太错误了,所以我强调了这些警告。


3
我同意JB Nizet的看法,即应该允许开发人员使用本机API。
但是,如果您需要为大量正则表达式匹配的方法提供默认答案,并且无法或不想修复这意味着的过多接口,则可以使用此SO答案作为灵感,使用默认答案来进行更安全的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);
    }
  }
}

应该为你模拟的方法调用答案方法吗?请看一下这个问题:https://stackoverflow.com/questions/65592904/how-to-mock-methods-by-reflection-using-mockito。 - uzzi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接