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