Search code examples
javajunitmockitovariadic-functionspowermock

PowerMockito / Mockito argument matcher call location issue


In short, I have a set of generated source code that I need to be able to dynamically mock based on external, non-Java configuration - they follow no consistent pattern / implement any interfaces other than being static, meaning I can only know how to mock a method at runtime and need to use PowerMockito to do so.

Say that I have this class:

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

And I simply want to mock doSomething / have it not throw exceptions. To do that simply / without any of the complexity I mention in my use case, I could do this:

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

Which works fine.

This changes however when we step back and try to address my needs, and move complexity to something like this:

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

Which leads to this issue: enter image description here

After much head scratching, managed to replicate this error much more succinctly:

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

Seemingly indicating that what's causing this issue is where the calls to create the argument matchers are, which is rather odd.


Solution

  • Got it - this isn't a PowerMockito thing. This is a standard Mockito thing and is actually by design - the telling point being one word in the error - you cannot use argument matchers outside of verification or stubbing. While I am using them for stubbing, the outside implies more.

    This led me to this answer to another question on how matchers work, with the comment of particular importance:

    - 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.
    

    Basically have to be careful with the stack, which led me to this, which works and meets my requirements of being able to mock variable number of arguments determined at runtime (the Strings in testDoSomething could all have been pulled from a text file and the method call could be managed via reflection):

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