I have a handful of very similar projects, all Java, SpringBoot, and Maven. All of them have one class with the same name, and almost identical contents. I added an additional method in one of them, the class with the problem I'm going to describe, but I'm pretty sure that detail is a coincidence.
Each project also has a test class corresponding to that very similar class, and the skeleton of that test class is identical in each class. The test class has an @InjectMocks
for the class under test (CUT), and two @Mock
annotations, one of which corresponds to an instance variable of the CUT.
The test class does have a @Before
method that creates an instance variable used by the tests.
All the variations of the test class have "@RunWith(MockitoJUnitRunner.class)
".
If I run one of the "good" tests and set a breakpoint on the first line of the @Before method and then look at the "this" variables in the variables pane, I see the types of the two @Mock-ed instance variables end with "$MockitoMock".
If I do the same thing in the "bad" test, the types of the two @Mock-ed variables do NOT end with "$MockitoMock
". In fact, these appear to be normal instances of the corresponding classes, not mocked classes.
Even more curious, in the "bad" test, I tried making explicit calls to "instvar = mock(clazz.class)
" in the @Before
method, and after I step over those, the type of the instance variable is STILL not a mocked type, HOWEVER, when I click on the instance variable, the toString panel shows "Mock for ..., hashCode: 1028811481
". If I "Resume" at this point, I can hit a breakpoint in the allegedly mocked class, with that same instance whose toString value says "Mock for ...
".
That's the issue in words. Now I guess I'll show some code.
Here's part of the "bad" test class:
@RunWith(MockitoJUnitRunner.class)
public class RestClientTest {
@InjectMocks
RestClient restClient;
@Mock
RestClientFactory restClientFactory;
@Mock
RestTemplate restTemplate;
HttpEntity<String> requestEntity;
@Before
public void setup() {
requestEntity = new HttpEntity<>(new HttpHeaders());
restClientFactory = mock(RestClientFactory.class);
restTemplate = mock(RestTemplate.class);
ReflectionTestUtils.setField(restClient, "restClientFactory", restClientFactory);
}
Here's part of the "good" test class:
@RunWith(MockitoJUnitRunner.class)
public class RestClientTest {
@InjectMocks
RestClient restClient;
@Mock
RestClientFactory restClientFactory;
@Mock
RestTemplate restTemplate;
HttpEntity<String> requestEntity;
@Before
public void setup() {
requestEntity = new HttpEntity<>(new HttpHeaders());
}
I've determined that both "good" and "bad" projects are using version 2.15.0 of mockito-core.
Update:
I tried stepping into the "mock" call in the bad test, and set breakpoints there, because it goes here from annotation processing, so I can see the behavior of both the bad and good case.
Here's what I saw in the good case:
I stepped over until line 65 and stepped into "createMock()". That put me in the MockUtil class:
The type of "mockMaker" is "org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker".
I stepped to line 35 and stepped into the "mockMaker.createMock()" method:
Now let's start over and run the "bad" case:
We first hit the initial breakpoint:
Now we see that the type of "mockMaker" is DIFFERENT from the good case. The type is "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker".
I'm not going to continue to step through this, but this path does produce the "fake mock" with the different toString value.
Now that I think about it, this instance is like a "spy" in that it's managed by Mockito, but all the methods call the original class method by default. I have no clue why it takes a different path here.
I would hope that this is enough information to give a clue to someone who better understands how this works.
The type of "mockMaker" [in the "good" case] is "org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker".
Now [in the "bad" case] we see that the type of "mockMaker" is DIFFERENT from the good case. The type is "org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker".
So, the "good" project is using the default mock-maker, which uses subclassing — see ByteBuddyMockMaker.java — while the "bad" project is using a non-default mock-maker that tries to use Java instrumentation to avoid subclassing: InlineByteBuddyMockMaker.java. That matches up with the behavior difference that you observed
According to the Javadoc for InlineByteBuddyMockMaker:
This mock maker must to be activated explicitly for supporting mocking final types and methods:
This mock maker can be activated by creating the file
/mockito-extensions/org.mockito.plugins.MockMaker
containing the textmock-maker-inline
ororg.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker
.
So to figure out why this is happening, you should search in your classpath to find out how the /mockito-extensions/org.mockito.plugins.MockMaker
resource is ending up in there.