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

问题描述

简而言之,我有一组生成的源代码,我需要能够根据外部的非 Java 配置动态模拟这些源代码——它们不遵循一致的模式/实现除静态之外的任何接口,这意味着我只能知道如何在运行时模拟方法,并且需要使用 powermockito 来执行此操作。

我有这门课:

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","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);
  }

导致此问题的原因:

enter image description here

经过多次摸索,设法更简洁地复制了此错误

  @Test
  public void testDoSomethingIssueIsolated() throws Exception {

    powermockito.spy(SomeClass.class);
    Object matcher = any(Integer.class);
    powermockito.donothing().when(SomeClass.class,matcher);

    SomeClass.doSomething(5);
  }

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

解决方法

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

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

- 呼叫顺序不仅重要,而且是使这一切正常工作的原因。 将匹配器提取到变量通常不起作用,因为它 通常会更改调用顺序。将匹配器提取到方法, 但是,效果很好。

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

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

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

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

我尝试了这个替代方案

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

这对我有用。

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

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

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

        PowerMockito.spy(classToTest);

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