Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inner class failed to access the mocked outer class method

Here are the sample class to be test:

public class TestOuterClass {
  private final InnerClass innerObj;

  boolean status;
  Object nullObj = null;

  public TestOuterClass(boolean status) {
    this.innerObj = new InnerClass();
    this.status = status;
  }

  public class InnerClass {
    public boolean doInner() {
      return outerMethod();
    }
  }

  public void mockOuterMethod() {
    if (innerObj.doInner()) {
      return;
    }
    throw new RuntimeException();
  }

  boolean outerMethod() {
    nullObj.toString();
    return status;
  }
}

The following is the example test code:

@RunWith(PowerMockRunner.class)
@PrepareForTest(TestOuterClass.class)
public class TestMockOuterClass {
  @Test
  public void testMockOuterMethod() {
    TestOuterClass outerObj = spy(new TestOuterClass(false));
    doReturn(true).when(outerObj).outerMethod();
    outerObj.mockOuterMethod();
  }

  @Test
  public void testOuterMethod() {
    TestOuterClass outerObj = spy(new TestOuterClass(false));
    doReturn(true).when(outerObj).outerMethod();
    assertTrue(outerObj.outerMethod());
  }
}

Note that the first test fails with the following exception stack and the second one succeeds:

java.lang.NullPointerException
at org.apache.samza.runtime.TestOuterClass.outerMethod(TestOuterClass.java:31)
at org.apache.samza.runtime.TestOuterClass$InnerClass.doInner(TestOuterClass.java:19)
at org.apache.samza.runtime.TestOuterClass.mockOuterMethod(TestOuterClass.java:24)
at org.apache.samza.runtime.TestMockOuterClass.testMockOuterMethod(TestMockOuterClass.java:22)
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:498)
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.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

It seems like when I created a spy object for class TestOuterClass and directly mock and test the outerMethod, it worked. However, the mock does not work when the outerMethod is triggered from InnerClass.doInner() method. What's the recommended way to fix the first test?

like image 234
Yi Pan Avatar asked Apr 28 '26 06:04

Yi Pan


1 Answers

Let's go through this example point by point.

In the test case, you create a spy around an instance of TestOuterClass. While creating the instance (in the constructor) you create an object of InnerClass as its field.

Now you mock the behaviour of the outerMethod to make it always return true.

Now, what happens when outerObj.mockOuterMethod() is called is that the real method is being called as it is not mocked. You can see it in the stacktrace which you provided:

at org.apache.samza.runtime.TestOuterClass.outerMethod(TestOuterClass.java:31)
at org.apache.samza.runtime.TestOuterClass$InnerClass.doInner(TestOuterClass.java:19)
at org.apache.samza.runtime.TestOuterClass.mockOuterMethod(TestOuterClass.java:24)

Later, it just goes on calling the real doInner method - and here is where it could be confusing - the real outerMethod.

You might say: "Hey, I've mocked that thing". But you see - in this method:

public boolean doInner() {
  return outerMethod();
}

You are actually refering to this.outherMethod(). The problem is that from the point of view of InnerClass, this refers to the actuall TestOuterClass instance - not the spy.

When you create a spy of an instance, a sort of proxy is being created. When you call a method which was mocked, then the real object won't be even reached. But when you call a method which was not mocked - it will "proxy" the call to the real object.

So your question is: how to fix it?

You can add another constructor of TestOuterClass in which instead of creating the instance of InnerClass you would inject it:

public TestOuterClass(boolean status, InnerClass innerObj) {
    this.innerObj = innerObj;
    this.status = status;
}

Then, in the tests you can create pass a mock of InnerClass to the constructor:

TestOuterClass.InnerClass innerObjMock = Mockito.mock(TestOuterClass.InnerClass.class);
TestOuterClass outerObj = spy(new TestOuterClass(false, innerObjMock));

With this setup you can simulate the behaviour of the innerObjMock.

If this is not something you were looking for, please let me know.

like image 139
Poger Avatar answered Apr 30 '26 21:04

Poger



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!