4

我们正在使用 Mock-Factory 为我们的开发人员提供关于模拟功能的最大可能舒适度,同时尽可能少地了解 mockito 本身。

为此,我们的 Mock-Factory 提供了一种在给定类名、方法名(通过正则表达式)和给定返回值的情况下创建模拟的方法,该返回值看起来如下(清理到此问题的相关部分):

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

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

谁能给我上面代码的原因或更好的替代方法?

4

3 回答 3

16

你不应该这样做。Mockito 是一个设计精良、易于学习、文档非常完善且几乎是事实上的标准框架。而且它是类型安全的,不需要反射,这使得测试易于阅读和理解。

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

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

于 2013-04-12T12:48:13.053 回答
8

好吧,您的方法并不是真正的好方法,它通常是过度设计开发人员的糖果乐园。即使您的团队是“年轻人”,他们也不必在使用 Mockito 时编写 ASM。另外,如果您采用这种方式,您将避免 Mockito 提供的简单性、表达性或可插入性方面的所有好处。作为一名建筑师,我宁愿确保我的工程师了解他们在做什么,而不是把他们放在婴儿公园里。否则他们怎么能成为一支伟大的球队?

此外,此处提供的实现可能过于简单,无法支持您在处理反射、桥方法、可变参数、覆盖等时可能遇到的所有情况。如果这段代码失败,它没有可理解的消息。简而言之,您失去了直接使用 Mockito 的所有好处,并且无论如何都向项目添加了不必要的东西。

编辑:刚刚看到JB Nizet 的回答,我完全同意他的看法。


但是,为了回答您的问题,那里发生了什么。简要看一下您的代码,您似乎不想关心传递给该方法的参数。

因此,假设您在被模拟的类中有以下真实方法:

String log2(String arg1, String arg2)

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

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

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

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

你写logN("a", "b", "c", "d")的就像普通的java。当您想使用参数匹配器时,您将使用 2 arg 方法编写此代码:

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

现在使用可变参数方法:

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,因此 Mockito 理解如果只有 3 个参数(可变参数被展平),此方法将匹配在第二种情况下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" });

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

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 实际上是数组)。这里的技巧是 vararg ininvoke(Object obj, Object... args)与被调用的方法 vararg 混淆了。

因此,在我的示例中使用 arg 匹配器,您应该这样做:

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 抽象到您的团队中,您将不得不管理许多班级级别的怪事。我不是说这是不可能的。但是作为此代码的所有者,您必须对其进行维护、修复并考虑可能出错的事情,并使此抽象代码的用户能够理解它。

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

于 2013-04-12T13:50:29.543 回答
3

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

但是,如果您确实需要为大量与正则表达式匹配的方法提供默认答案,并且您不能或不会修复这暗示的超重接口,那么您可以使用这个 SO 答案作为灵感来使用默认的 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);
    }
  }
}
于 2013-04-15T23:06:38.640 回答