4

I am new to Mockito, and having an issue that took lot of my time. Below is my problem statement and executable code.

Problem

Whenever i try to mock multiple behaviours from same method with different arguments mockito/powermockito use the last behaviour which i defined for test in a single test.Below is my example, Service class has a static foo method that is being called from my method (that i want to test) number of times with different parameters.

It throws ClassCastException and want to cast BResponse into AResponse, because my last stubbing if for BResponse whereas my first call for foo in ClassUnderTest.execute() demands AResponse.

Sample Code

package poc.staticmethod;

import static org.mockito.Matchers.any;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

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

import lombok.AllArgsConstructor;
import lombok.Data;

@RunWith(PowerMockRunner.class)
@PrepareForTest(PocStaticTest.Service.class)
public class PocStaticTest {
  

  @InjectMocks
  ClassUnderTest c = new ClassUnderTest();
  
  @Before
  public void beforeTest() throws Exception {
    mockStatic(Service.class);
  }

  @Test
  public void myTest() {
    when(Service.foo(any(), new ARequest(any(), "A"))).thenReturn(new AResponse(1, "passed"));
    when(Service.foo(any(), new ARequest(any(), "2A")))
        .thenReturn(new AResponse(2, "passed"));
    when(Service.foo(any(), new BRequest(any(), "B")))
        .thenReturn(new BResponse(112, "passed"));
    
    c.execute();
  }

  public class ClassUnderTest {
    public void execute() {
      AResponse ar = (AResponse) Service.foo("A1", new ARequest(1, "A"));
      AResponse ar2 = (AResponse) Service.foo("A2", new ARequest(2, "2A"));
      BResponse br = (BResponse) Service.foo("B1", new BRequest(1, "B"));
    }
  }
  
  public static class Service {
    public static Object foo(String firstArgument, Object obj) {
      return null;
    }
  }
  
  @Data
  @AllArgsConstructor
  public class ARequest {
    public Integer num;
    public String name;
  }

  @Data
  @AllArgsConstructor
  public class AResponse {
    public Integer error;
    public String message;
  }

  @Data
  @AllArgsConstructor
  public class BRequest {
    public Integer num;
    public String name;
  }

  @Data
  @AllArgsConstructor
  public class BResponse {
    public Integer error;
    public String message;
  }

}

Exception:

java.lang.ClassCastException: poc.staticmethod.PocStaticTest$BResponse cannot be cast to poc.staticmethod.PocStaticTest$AResponse
    at poc.staticmethod.PocStaticTest$ClassUnderTest.execute(PocStaticTest.java:44)
    at poc.staticmethod.PocStaticTest.myTest(PocStaticTest.java:39)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.junit.internal.runners.TestMethod.invoke(TestMethod.java:68)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$PowerMockJUnit44MethodRunner.runTestMethod(PowerMockJUnit44RunnerDelegateImpl.java:316)
    at org.junit.internal.runners.MethodRoadie$2.run(MethodRoadie.java:89)
    at org.junit.internal.runners.MethodRoadie.runBeforesThenTestThenAfters(MethodRoadie.java:97)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$PowerMockJUnit44MethodRunner.executeTest(PowerMockJUnit44RunnerDelegateImpl.java:300)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner.executeTestInSuper(PowerMockJUnit47RunnerDelegateImpl.java:131)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner.access$100(PowerMockJUnit47RunnerDelegateImpl.java:59)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner$TestExecutorStatement.evaluate(PowerMockJUnit47RunnerDelegateImpl.java:147)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner.evaluateStatement(PowerMockJUnit47RunnerDelegateImpl.java:107)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit47RunnerDelegateImpl$PowerMockJUnit47MethodRunner.executeTest(PowerMockJUnit47RunnerDelegateImpl.java:82)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$PowerMockJUnit44MethodRunner.runBeforesThenTestThenAfters(PowerMockJUnit44RunnerDelegateImpl.java:288)
    at org.junit.internal.runners.MethodRoadie.runTest(MethodRoadie.java:87)
    at org.junit.internal.runners.MethodRoadie.run(MethodRoadie.java:50)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl.invokeTestMethod(PowerMockJUnit44RunnerDelegateImpl.java:208)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl.runMethods(PowerMockJUnit44RunnerDelegateImpl.java:147)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$1.run(PowerMockJUnit44RunnerDelegateImpl.java:121)
    at org.junit.internal.runners.ClassRoadie.runUnprotected(ClassRoadie.java:34)
    at org.junit.internal.runners.ClassRoadie.runProtected(ClassRoadie.java:44)
    at org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl.run(PowerMockJUnit44RunnerDelegateImpl.java:123)
    at org.powermock.modules.junit4.common.internal.impl.JUnit4TestSuiteChunkerImpl.run(JUnit4TestSuiteChunkerImpl.java:121)
    at org.powermock.modules.junit4.common.internal.impl.AbstractCommonPowerMockRunner.run(AbstractCommonPowerMockRunner.java:53)
    at org.powermock.modules.junit4.PowerMockRunner.run(PowerMockRunner.java:59)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)  
Community
  • 1
  • 1
Khalid Shah
  • 3,132
  • 3
  • 20
  • 39

2 Answers2

6

use eq() matcher for your custom object and by doing this change your function mocking would looks linke:

when(Service.foo(any(), eq(new ARequest(1, "A")))).thenReturn(new AResponse(1, "passed"));
when(Service.foo(any(), eq(new ARequest(2, "2A")))).thenReturn(new AResponse(2, "passed"));
when(Service.foo(any(), eq(new BRequest(1, "B")))).thenReturn(new BResponse(112, "passed"));

you should specify the arguments in your Request objects and remove any() from inside.

alternative option is write your answer and check types inside it, like:

when(mock.foo(anyString(), anyObject())).thenAnswer(
    invocation -> {
        Object argument = invocation.getArguments()[1];
        if (argument.equals(new ARequest(1, "A"))) {
            return new AResponse(1, "passed");
        } else if (argument.equals(new ARequest(2, "2A"))) {
            return new AResponse(2, "passed");
        } else if (argument.equals(new BRequest(1, "B"))) {
            return new BResponse(112, "passed");
        }
        throw new InvalidUseOfMatchersException(
            String.format("Argument %s does not match", argument)
        );
    }
);
Zeeshan Bilal
  • 1,147
  • 1
  • 8
  • 22
1

Guessing: I think you have a wrong understanding of those mocking specs.

You wrote:

when(Service.foo(any(), new ARequest(any(), "A")))

But does that really make sense? You want to say: when foo() is invoked, the first parameter doesn't matter. The second one has to be some ARequest object. More specifically, you want to say: an ARequest object which ignores its "number" part when comparing to others. But: that is not how things will work out here. You have to provide a real object there, not a "spec'ed" one!

What I mean is: later, at runtime, the mocking framework sees that foo() is called, and then it starts looking for matching parameters. But how is the mocking framework supposed to understand that you want to match ARequest objects that contain arbitrary numbers?

In other words: I assume that Lombok @Data will generate equals methods. And those will of course compare all elements in those classes.

So, I the solution could be to use specific request objects, like

when(Service.foo(any(), new ARequest(1, "A")))

to then make sure that the numeric ID is always coming in as 1.

Alternatively, you could try to generate equals() methods that only compare the "name" part of your requests.

Long story short: I think you are getting the semantics of these when-specifications wrong.

I think what happens is that new ARequest(any(), "A") just creates some kind of ARequest object, and then PowerMockito will try to equal that; and the result of that is probably "false" all the time.

Finally: I hope you understand that you are mixing up TWO bytecode manipulation frameworks has a certain chance of causing bizarre problems? I have seen many occasions when PowerMock(ito) caused strange fails (as it is changing your bytecode in certain ways); and then you add the lombok stuff on top of that? And you are even going for mocking static calls? Thus my personal recommendation: see if there is any chance to turn that static method into some instance method (you could even just put your own wrapper around the static call) so that you can use ordinary Mockito instead of PowerMockito here!

GhostCat
  • 137,827
  • 25
  • 176
  • 248