So, I owe Jim an apology. He’d written a working mockito and JUnit test, and I told him in review that I didn’t think it did what he expected it to. While I was wrong, this scenario reads like a bug to me. Call it desirable unexpected side effects.
Imagine you have the following two classes:
Nothing exciting there…
Now let’s try to test the service with a Mockito test (JUnit 5 here):
The test passes. But should it?
What’s InjectMocks for?
To me, the
@InjectMocks annotation is intended to be a factory method to create something that depends on mock values, expressed with
@Mock in the test. That’s how I commonly use it, and I also expect that all objects in my ecosystem are built using constructor injection.
This is a good design principle, but it’s not the definition of what the tool does!
What InjectMocks does…
The process of applying this annotation looks at the field annotated with
@InjectMocks and takes a different path if it’s
null than if it’s already initialized. Being so purist about the
null path being a declarative constructor injection approach, I’d completely not considered that to inject mocks can mean to do that to an existing object. The documentation doesn’t quite make this point either.
- If there’s no object, then
@InjectMocksmust create one
- It uses the biggest constructor it can supply to
- If there is an object, it tries to fill in mocks via setters
- If there no setters it tries to hack mocks in by directly setting the fields, forcing them to be accessible along the way
To top it all,
@InjectMocks fails silently, so you can have mystery test failures without knowing it.
To top it all further, some people using
MockitoAnnotations.initMocks() calls in their tests, on top of the Mockito Runner, which causes all manner of oddness!!! Seriously guys, NEVER CALL THIS.
Er… sorry Jim!
@InjectMocksannotation does try to do the most helpful thing it can, but the more complex the scenario, the harder it is to predict.
Using two cross-cutting techniques to initialize an object feels to me like a dangerous and hard to fathom approach, but if it works, then it may be better than the alternatives, so long as it’s documented. Add a comment!
Maybe there needs to be some sort of
@InjectWithFactory where you can declare a method that receives the mocks you need and have that called at construction with the
@Mock objects, for you to fill in any other parameters from the rest of the test context.
Or maybe we just get used to this working and forget about whether it’s easy to understand.
I found out what Mockito does in the above by creating a test and debugging the Mockito library to find how it achieves the outcome. I highly recommend exploring your most commonly used libraries this way. You’ll learn something you’ll find useful!
Opinions expressed by Java Code Geeks contributors are their own.