A Surprising Injection
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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | public class Service { private String name; private Widget widget; public Service(String name, Widget widget) { this .name = name; this .widget = widget; } public void execute() { widget.handle(name); } } public interface Widget { void handle(String thing); } |
Nothing exciting there…
Now let’s try to test the service with a Mockito test (JUnit 5 here):
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | @ExtendWith (MockitoExtension. class ) class ServiceTest { @Mock private Widget widget; @InjectMocks private Service service = new Service( "Joe" , widget); @Test void foo() { service.execute(); verify(widget).handle( "Joe" ); } } |
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
@InjectMocks
must 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.
Lessons Learned
Er… sorry Jim!
The @InjectMocks
annotation 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.
Final Thought
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!
Published on Java Code Geeks with permission by Ashley Frieze, partner at our JCG program. See the original article here: A Surprising Injection Opinions expressed by Java Code Geeks contributors are their own. |