Search code examples
javajunitdependency-injectionmockitoguice

Java: Junit a class with Inject annotation


@Singleton
public class RealWorkWindow implements WorkWindow {

    @Inject private Factory myFactory;

    public RealWorkWindow (
            LongSupplier longSupplier
    ) {
        defaultWindow = myFactory.create(() -> 1000L);
        workWindow = myFactory.create(longSupplier);
    } 
    ...

As you can see I am Injecting a factory class (injected via FactoryModuleBuilder)

Test code

@Test
public class RealWorkWindowTest {
    private RealWorkWindow testWindow;

    @BeforeMethod
    void setup() {
        MockitoAnnotations.initMocks(this);

        testWindow = spy(new RealWorkWindow(() -> 1L));
    }

Factory.py

public interface RealWorkWindowFactory {
    RealWorkWindowFactory create(LongSupplier longSupplier);
}

Module

install(new FactoryModuleBuilder()
                        .implement(WorkWindow.class, RealWorkWindow.class)
                        .build(RealWorkWindowFactory.class));

When I run the test RealWorkWindowTest the test fails with NPE that factory does not exists, which makes sense since I dont think injection runs.

How can I test with Injection in junit? or mock properly?

Similar to the problem describe in https://mhaligowski.github.io/blog/2014/05/30/mockito-with-both-constructor-and-field-injection.html

But the problem that I have is that mock is used IN constructor so it's still a null when instantiate the test object (because i have not called Mockito.init yet)


Solution

  • Use constructor injection when using @Assisted injection

    Guice's Assisted Injection wiki page mentions:

    AssistedInject generates an implementation of the factory class automatically. To use it, annotate the implementation class' constructor and the fields that aren't known by the injector:

    And later:

    AssistedInject maps the create() method's parameters to the corresponding @Assisted parameters in the implementation class' constructor. For the other constructor arguments, it asks the regular Injector to provide values.

    As they are only available at that time, Guice will only inject fields after the constructor call. This translates for you in the fact that you must use the constructor injection, and no other mechanism (unless you have an extension that allows @PostConstruct or similar).

    So let's rewrite your code according to that. Write your RealWorkWindow as follow:

    @Singleton
    public class RealWorkWindow implements WorkWindow {
    
      private final WorkWindow defaultWindow;
      private final WorkWindow workWindow;
    
      @Inject
      public RealWorkWindow(Factory myFactory, @Assisted LongSupplier longSupplier) {
        defaultWindow = myFactory.create(() -> 1000L);
        workWindow = myFactory.create(longSupplier);
      }
    
    }
    

    Your code can then become testable as follows:

    @RunWith(MockitoJunitRunner.class)
    public class RealWorkWindowTest {
    
      @Mock
      Factory myFactory;
    
      @Mock
      WorkWindow defaultWindow;
    
      @Mock
      WorkWindow workWindow;
    
      RealWorkWindow realWorkWindow;
    
      @BeforeEach
      void setup() {
        when(myFactory.create(any(LongSupplier.class)))
            .thenReturn(defaultWindow) // First call
            .thenReturn(workWindow);   // Second call
        realWorkWindow = new RealWorkWindow(myFactory, () -> 1000L);
      }
    
    }