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:

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

@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 @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.

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!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s