javaunit-testingmockitostubpartial-mocks

spy annotated with mockito @Spy does not initialize @Mock fields


I have a class:

@AllArgsConstructor
public class QuestionInfoService {
    QuestionInfoRepository repository;
}

I would like to create a test where I create a spy for this class:

@Spy QuestionInfoService questionInfoService 

At the same time I would like questionInfoService to be initialized with mocked QuestionInfoRepository :


@RunWith(MockitoJUnitRunner.class)
public class QuestionInfoServiceTest {

    @Mock
    QuestionInfoRepository repository;

    @Spy
    QuestionInfoService questionInfoService = new QuestionInfoService(repository);

    @Test
    public void shouldFetchQuestions() {
        System.out.println(questionInfoService.getRepository());
    }
}

According to documentation I should be able to do that:

@Spy Foo spyOnFoo = new Foo("argument");

https://javadoc.io/doc/org.mockito/mockito-core/2.8.47/org/mockito/Spy.html

Although when I run my tests the results are:

null

How can I initialize the @Spy with the field annotated with @Mock ?


Solution

  • There's a small but crucial difference between your test and the example from the Mockito docs:

    @Spy Foo spyOnFoo = new Foo("argument");
    

    passes a constant, literal value to the constructor of Foo, whereas

    @Mock QuestionInfoRepository repository;
    @Spy QuestionInfoService questionInfoService = new QuestionInfoService(repository);
    

    passes the value of an uninitialized field to the constructor of QuestionInfoService.

    Mockito/JUnit need to create an instance of your test class (one instance per test method). The field initializers are executed at the time of initialization, and only after that can Mockito assign values to the annotated fields. Since repository was null at initialization time, the QuestionInfoService instance was already created with a null repository and then assigned to the field. Later assignments to repository have zero effect on the existing QuestionInfoService instance.

    The solution? Manually instruct Mockito to create the spy in your setup method:

    @Mock QuestionInfoRepository repository;
    QuestionInfoService questionInfoService;
    
    @Before
    void setup() {
      // here, `repository` has already been assigned a value by Mockito via the annotation:
      questionInfoService = spy(new QuestionInfoService(repository));
    }
    

    A similar problem (albeit without @Spy annotation), its cause and the solution is outlined in Corollary: Object life cycles and magic framework annotations.

    References: