简体   繁体   中英

The deep mock is not working as member of @InjectMocks

The annotation @InjectMocks brings us the way to stub/shim private member and reuse the testing case. Here is the concept code where issue happened when we shim the Fake Member.

  public class TestBuilder{
      @Spy
      private StubComponent componentA = new StubComponent();
      @Mock
      private FakeComponent componentB;
      @InjectMocks
      private class TestTarget targetInstance = mock(TestTarget.class);

      public static Class TestTarget{
        private StubComponent componentA;
        private FakeComponent componentB;
        public ShimmedResultB testInvokation(String para){
            componentA.doCallRealMethod();
            ShimmedResultA shimmedResultA = componentA.someUnableToStubbedMethod(para);
            ShimmedResultB shouldNotBeNull = componentB.someShimmedMethod(shimmedResultA);
            return shouldNotBeNull;
        }
      }

      private TestBuilder(){
        MockitoAnnotations.initMocks(this);
        //Shim the real component A with partial stubbed
        doReturn(shimmedResultA).when(componentA).someUnableToStubbedMethod(any());
        //Shim the fake component B
        //************The issue is here****************
        componentB = mock(FakeComponent.class);
        //*********************************************
        when(componentB.someShimmedMethod(any())).thenReturn(shimmedResultB);

      }
      public TestTarget getTargetInstance(){
        return this.targetInstance;
      }

      public static TestTarget build(){
        return (new TestBuilder()).getTargetInstance();
      }

      public static main(String[] args){
        TestTarget testInstance = TestBuilder.build();
        ShimmedResultB result = testInstance.testInvokation("");
        assertThat(result, not(equalTo(null)));
      }
  }

The issue is when we mock the Fake componentB . Then the someShimmedMethod will return null. It seems the InjectMocks could not carry the mock() to private member.

Here are some terminology definition:

  1. StubComponent: The test would penetrate to this component as private member. However, there is some method might not be able to go through. We could shim its public method. This component may have smaller scope of dependencies which are easily to be initiated by local resource.

  2. FakeComponent: This component would be testing in somewhere else. Here we can only build the mocked instance and shim all the methods would be leveraged by test target.

  3. Stub: The @Spy could help us to hook the Stubbed member. The private member is not 100% real. But some stubbed part could let test penetrate into this private member.

  4. Shim: The @Mock would give us a null pointer until initMocks. So we can start to design the return of Fake component after initMocks. This is magic of @InjectMocks. However, this is the most tricky part since Developer would like to initiate every thing and mock(FakeComponent.class) for componentB intuitively. That will clean all the shimmed design and make your assertion failed.

==================================================================

Thanks for Maciej's Answer and sorry about the typo when I translate the structure of my test case. And let me raise the issue with more clear description to Maciej's Answer.

  public class TestBuilder{
      @Spy
      private StubComponent componentA = new StubComponent();
      @Mock
      private FakeComponent componentB;
      @InjectMocks
      private TestTarget targetInstance = mock(TestTarget.class);

      public static Class TestTarget{
        private StubComponent componentA;
        private FakeComponent componentB;
        public ShimmedResultB testInvokation(String para){
            componentA.doCallRealMethod();
            ShimmedResultA shimmedResultA = componentA.someUnableToStubbedMethod(para);
            ShimmedResultB shouldNotBeNull = componentB.someShimmedMethod(shimmedResultA);
            return shouldNotBeNull;
        }

        public TestTarget(){
            //The FakeComponent has some specific remote resource
            //And could not be initialized here
            componentB = new FakeComponent();
            //We will use mock server to test this FakeComponent else where
        }
      }

      private TestBuilder(){
        //Hook the testing Function for trigger the step in
        doCallRealMethod().when(this.targetInstance).testInvokation(anyString());
        //Inject Stubbed and Faked Private Member for testing
        MockitoAnnotations.initMocks(this);
        //Shim the real component A with partial stubbed
        doReturn(shimmedResultA).when(componentA).someUnableToStubbedMethod(any());

        //************The issue is here****************
        componentB = mock(FakeComponent.class);
        //*********************************************
        //Shim the leveraged method of fake componentB
        when(componentB.someShimmedMethod(any())).thenReturn(shimmedResultB);
      }

      public TestTarget getTargetInstance(){
        return this.targetInstance;
      }

      public static TestTarget build(){
        return (new TestBuilder()).getTargetInstance();
      }

      public static main(String[] args){
        TestTarget testInstance = TestBuilder.build();
        //The doRealCall hook will trigger the testing
        ShimmedResultB result = testInstance.testInvokation("");
        assertThat(result, not(equalTo(null)));
      }
  }

There are something added in second concept code:

  1. The componentB is a scope we don't want to step in. However, the TestTarget has initiation with componentB in its constructor. This is pretty common when we have a utility related to remote source. We use to test componentB independently with mock server or other technique. Hence, we can only use mock(TestTarget.class).

  2. Since we mocked the TestTarget. There is one thing I missed that we need to use doCallRealMethod().when(targetInstance) to trigger the testInvokation(). And this constrains the null declairation of targetInstance. We need mock() and hook the doCallRealMethod.

So the consequence is we need to left @Mock as null without any mock() to let @InjectMocks to handle the shim. We just found this is tricky when we use @InjectMocks.

The problem is with the @InjectMocks definition:

@InjectMocks
private class TestTarget targetInstance = mock(TestTarget.class);

The class under test should never be a mock (also why the class keyword).

Try to use:

@InjectMocks
private TestTarget targetInstance = new TestTarget();

or simply:

@InjectMocks
private TestTarget targetInstance;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM