PowerMockito / Mockito 参数匹配器调用位置问题

PowerMockito / Mockito argument matcher call location issue

简而言之,我有一组生成的源代码,我需要能够基于外部的非Java配置动态模拟它们——它们不遵循一致的模式/实现任何接口,而不是static,意味着我只能知道如何在运行时模拟一个方法,需要使用 PowerMockito 来实现。

说我有这个class:

public class SomeClass {
  public static void doSomething(Integer i) {
    throw new RuntimeException();
  }
}

我只是想模拟 doSomething /让​​它不抛出异常。为了简单地/没有我在用例中提到的任何复杂性,我可以这样做:

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest(SomeClass.class)
public class TestSomeClass {

  @Test
  public void testDoSomethingSimple() throws Exception {

    PowerMockito.spy(SomeClass.class);
    PowerMockito.doNothing().when(SomeClass.class, "doSomething", any(Integer.class));

    SomeClass.doSomething(5);
  }
}

效果很好。

然而,当我们退后一步并尝试解决我的需求,并将复杂性转移到这样的事情时,情况就会发生变化:

  @Test
  public void testDoSomething() throws Exception {
    // Below showing how everything could be externally-driven
    testDoSomething("SomeClass", "doSomething", "java.lang.Integer");
    SomeClass.doSomething(5);
  }

  public void testDoSomething(
      final String canonicalClassName, final String methodName, final String... canonicalParameterClassNames)
      throws Exception {

    final Class<?> clazz = Class.forName(canonicalClassName);

    PowerMockito.spy(clazz);

    final Object[] argumentMatchers = new Object[canonicalParameterClassNames.length];
    for (int i = 0; i < canonicalParameterClassNames.length; i++) {
      argumentMatchers[i] = any(Class.forName(canonicalParameterClassNames[i]));
    }

    PowerMockito.doNothing().when(clazz, methodName, argumentMatchers);
  }

导致此问题的原因:

经过一番摸索,设法更简洁地重现了这个错误:

  @Test
  public void testDoSomethingIssueIsolated() throws Exception {

    PowerMockito.spy(SomeClass.class);
    Object matcher = any(Integer.class);
    PowerMockito.doNothing().when(SomeClass.class, "doSomething", matcher);

    SomeClass.doSomething(5);
  }

似乎表明导致此问题的原因是其中创建参数匹配器的调用,这很奇怪。

如果你仔细阅读失败跟踪,你就会找到这个问题的答案

Misplaced or misused argument matcher detected here:

-> at mockito.TestSomeClass.testDoSomething(TestSomeClass.java:xx)

You cannot use argument matchers outside of verification or stubbing.
Examples of correct usage of argument matchers:
    when(mock.get(anyInt())).thenReturn(null);
    doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject());
    verify(mock).someMethod(contains("foo"))

您尝试在 for 循环中使用 any(...),这在验证或存根之外(此处:PowerMockito.doNothing().when(...))。

for (int i = 0; i < canonicalParameterClassNames.length; i++) {
  argumentMatchers[i] = any(Class.forName(canonicalParameterClassNames[i]));
}

PowerMockito.doNothing().when(clazz, methodName, argumentMatchers);

因此,您的解决方案将不起作用。

我试过这个替代方案

    for (int i = 0; i < canonicalParameterClass.length; i++) {
        PowerMockito.doNothing().when(clazz, methodName, any(Class.forName(canonicalParameterClass[i])));
    }

这对我有用。

您可以通过使用 Class 而不是 String 作为您的 class 名称来简化您的方法。

    @Test
    public void testDoSomething() throws Exception {
        // Below showing how everything could be externally-driven
        testDoSomething(SomeClass.class, "doSomething", Integer.class);
        SomeClass.doSomething(5);
    }

    public void testDoSomething(final Class classToTest, final String methodName, final Class... parameterClasses)
            throws Exception {

        PowerMockito.spy(classToTest);

        for (int i = 0; i < parameterClasses.length; i++) {
            PowerMockito.doNothing().when(classToTest, methodName, any(parameterClasses[i]));
        }
    }
}

知道了 - 这不是 PowerMockito 的东西。这是一个标准的 Mockito 东西,实际上是设计使然——告诉点是错误中的一个词——你不能在验证之外使用参数匹配器 或存根。当我使用它们进行存根时,外部意味着更多。

这让我想到了 this answer to another question on how matchers work,其中的评论特别重要:

- Call order isn't just important, it's what makes this all work. Extracting matchers to variables generally doesn't work, because it usually changes the call order. Extracting matchers to methods, however, works great.

int between10And20 = and(gt(10), lt(20));
/* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
// Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().

public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
/* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
// The helper method calls the matcher methods in the right order.

基本上必须小心堆栈,这让我想到了这个,它可以工作并满足我的要求,即能够模拟在运行时确定的可变数量的参数(testDoSomething 中的字符串都可以从文本文件和方法调用可以通过反射进行管理):

  @Test
  public void testDoSomething() throws Exception {
    // Below showing how everything could be externally-driven
    mockAnyMethod("SomeClass", "doSomething", "java.lang.Integer");
    SomeClass.doSomething(5);
  }

  public void mockAnyMethod(
      final String canonicalClassName,
      final String methodName,
      final String... canonicalParameterClassNames)
      throws Exception {

    final Class<?> clazz = Class.forName(canonicalClassName);

    PowerMockito.spy(clazz);

    PowerMockito.doNothing()
        .when(clazz, methodName, getArgumentMatchers(canonicalParameterClassNames));
  }

  public Object[] getArgumentMatchers(final String... canonicalParameterClassNames)
      throws ClassNotFoundException {

    final Object[] argumentMatchers = new Object[canonicalParameterClassNames.length];
    for (int i = 0; i < canonicalParameterClassNames.length; i++) {
      argumentMatchers[i] = any(Class.forName(canonicalParameterClassNames[i]));
    }
    return argumentMatchers;
  }