52

I'm writing a test case for a Class which has a 2 level of dependency injection. I use @Spy annotation for the 1 level dependency injection object, and I would like to Mock the 2nd level of injection. However, I kept getting null pointer exception on the 2nd level. Is there any way that I inject the mock into the @Spy object?

public class CarTestCase{
    @Mock
    private Configuration configuration;

    @Spy 
    private Engine engine;

    @InjectMocks 
    private Car car;

    @Test
    public void test(){

       Mockito.when(configuration.getProperties("")).return("Something");
       car.drive();
    }

}

public class Car{
    @Inject
    private Engine engine;

    public void drive(){
        engine.start();
    }
}

public class Engine{
    @Inject 
    private Configuration configuration;

    public void start(){
        configuration.getProperties();   // null pointer exception
    }

}
Wildchild
  • 535
  • 1
  • 5
  • 5

6 Answers6

56

I've also wandered how to inject a mock into a spy.

The following approach will not work:

@Spy
@InjectMocks
private MySpy spy;

But the desired behavior can be achieved by a "hybrid" approach, when using both annotation and manual mocking. The following works perfectly:

@Mock
private NeedToBeMocked needToBeMocked;

@InjectMocks
private MySpy mySpy;

@InjectMocks
private SubjectUnderTest sut;

@BeforeMethod
public void setUp() {
    mySpy = Mockito.spy(new MySpy());
    MockitoAnnotations.initMocks(this);
}

(SubjectUnderTest here depends on MySpy, and MySpy in its turn depends on NeedToBeMocked).

UPD: Personally, I think that if you have to do such a magic too often, it might be a sign that there is something wrong with dependenicies between your classes and it is worth to perform a little bit of refactoring to improve your code.

Yoory N.
  • 4,881
  • 4
  • 23
  • 28
  • 4
    To be honest, I don't really get it. But it worked perfectly :) – spyro Sep 07 '18 at 08:34
  • To be clear, this works, but it seems to work through "magic". I'm happily using it to solve my short term problem (and upvoted it), but I get the uneasy feeling that this could break at any time :-) – JP Belanger Feb 26 '19 at 16:03
  • 1
    @JPBelanger, yes, this approach can probably break in future versions of Mockito if they will change the way they create mocks and spies. To see, how this works at the moment, you can stop at a debugger breakpoint inside a `@Test` method and look what's inside a `mySpy` variable. There is an instance of `MySpy` object with modified methods that do their additional stuff and then call the original methods. All fields are still there and can be injected by `@InjectMocks`. – Yoory N. Feb 28 '19 at 14:17
38

The (simplest) solution that worked for me.

@InjectMocks
private MySpy spy = Mockito.spy(new MySpy());

No need for MockitoAnnotations.initMocks(this) in this case, as long as test class is annotated with @RunWith(MockitoJUnitRunner.class).

Ahmad Shahwan
  • 1,662
  • 18
  • 29
25

Mockito cannot perform such a tricky injections as it's not an injection framework. So, you need to refactor your code to make it more testable. It's easy done by using constructor injection:

public class Engine{
    private Configuration configuration;

    @Inject 
    public Engine(Configuration configuration) {
        this.configuration = configuration;
    }
    ........
}

public class Car{
    private Engine engine;

    @Inject    
    public Car(Engine engine) {
        this.engine = engine;
    }
}

In this case you have to handle the mocking and injection manually:

public class CarTestCase{

    private Configuration configuration;

    private Engine engine;

    private Car car;

    @Before
    public void setUp(){
        configuration = mock(Configuration.class);
        engine = spy(new Engine(configuration));
        car = new Car(engine);
    }

    @Test
    public void test(){

       Mockito.when(configuration.getProperties("")).return("Something");
       car.drive();
    }

}
Sergii Bishyr
  • 8,331
  • 6
  • 40
  • 69
  • 2
    I see, but is it the general design in Spring framework that people would put the injection in the constructor? Then what's the purpose of having Inject annotation? – Wildchild May 15 '17 at 06:28
  • @Wildchild works the same. You don't have to call constructor explicitly, Spring will do it for you if you are in the Spring context. Moreover, the field injection is not recommended, it's always better to use constructor injection. – Sergii Bishyr May 15 '17 at 07:13
17

I also met this issue during the unit testing with Spring boot framework, but I found one solution for using both @Spy and @InjectMocks

Previous answer from Yoory N.

@Spy
@InjectMocks
private MySpy spy;

Because InjectMocks need to have instance created, so the solution works for me is at below,

@Spy
@InjectMocks
private MySpy spy = new MySpy();
Jack Yang
  • 171
  • 1
  • 3
  • 3
    Doesn't work for me. I've got `NotAMockException` using version 2.21.0 of Mockito. On another project with ancient Mockito 1.10.19 this works **not** as expected -- the dependencies are injected, but the `spy` field is not a real spy and I cannot do things like `Mockito.verify(mySpy).someMethodCall();`. – Yoory N. Sep 07 '18 at 10:22
  • Worked for me. JUnit 4.12, Mockito 1.10.19 and we have also PowerMock 1.6.4 – MicNeo Apr 01 '20 at 00:37
  • When using with `@ExtendWith(MockitoExtension.class)` it **doesn't work**, you need to use `MockitoAnnotations.openMocks(this);` in a `@BeforeEach` setup method **for it to work**. (tested with Mockito v3.12.4 et junit-jupiter v5.8.1) – Sylhare Oct 06 '21 at 20:10
4

I think I just found the definitive answer. I tried Yoory approach but changed the order of the annotations :

@InjectMocks
@Spy
private MySpy spy;

I assume that Mockito first creates the mock, and adds a spy on top of that. So there is no need to instantiate the MySpy object.

DavidBu
  • 478
  • 4
  • 6
  • 3
    Nice catch! This way the `spy` is a real Mockito's spy and all fields are injected. But now it fails to inject this spy into `SubjectUnderTest` instance using `@InjectMocks` (as [in my example](https://stackoverflow.com/a/50784746/8868289)) and I get NullPointerException when it tries to call spy's methods. Tested on Mockito 2.21.0. Have no idea yet on how to overcome this. – Yoory N. Feb 28 '19 at 14:53
0

Junit5 + mockito-junit-upiter-4.2.0 works well

@ExtendWith(MockitoExtension.class)
public class MyTest {

@Spy
@InjectMocks
private SomeClass spy = new SomeClass();
Mike
  • 20,010
  • 25
  • 97
  • 140