简体   繁体   中英

Mockito and Javax annotation @PostConstruct

I'm writing a Java unit test using JUnit Jupiter for a class which has a method with a javax.annotation.PostConstruct annotation.

I want to test a method in the class but am hitting an issue where the @PostConstruct method is always called once the object is created (unsurprisingly). The Mockito mock object I've created to be used in the @PostConstruct method isn't providing the results I require.

Here's the method that is being called when the object is created:

@Service
@Slf4j
public class MyService {
    @Inject
    private MyRepository myRepository;

    private Integer myId;

    @PostConstruct
    public void postInit() {
        MyObject myObj = myRepository.findByName("FOO");
        myId = myObj.getId();
    }

    public void myMethod() {
        ... code to be tested here...
    }
}

My JUnit test class contains the following:

@ExtendWith(SpringExtension.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class MyServiceTest {
    private static final Integer ID = 0;

    @Test
    public void myTest() {
        // Given.
        MyObject myObj = new MyObject();
        myObj.setId(ID);

        MyRepository myRepository = Mockito.mock(MyRepository.class);
        Mockito.when(myRepository.findByName(anyString())).thenReturn(myObj);

        // Test runs here.
        myService.myMethod()
    }
}

When I run the unit test, I can see that the mock has been instantiated via my IDE. However, it always returns a NULL object when the findByName("FOO") method is called, even though it should return a correct instance of MyObject .

As such, the unit test fails with a NullPointerException because myObj is null.

Is there anything obviously incorrect that I'm doing here>?

Thanks in advance for any assistance.

I've modified your test slightly, creating MyService via Mockito and making myMethod return the id:

@ExtendWith(SpringExtension.class)
class MyServiceTest {
    private static final Integer ID = 42;

    @InjectMocks
    MyService myService;

    @Test
    public void myTest() {
        // Given.
        MyObject myObj = new MyObject();
        myObj.setId(ID);

        MyRepository myRepository = Mockito.mock(MyRepository.class);
        Mockito.when(myRepository.findByName(anyString())).thenReturn(myObj);

        // Test runs here.
        assertThat(myService.myMethod()).isEqualTo(ID);
    }
}

When I run this, I get:

java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "this.myId" is null

    at MyService.myMethod(MyService.java:24)
    at MyServiceTest.myTest(MyServiceTest.java:28)
    ...

So clearly our mocking of findByName hasn't worked.

That's because myRepository is just a local variable in our test -- there's nothing to get it injected into MyService .

One way to fix this is not to use spring test infrastructure, just plain Mockito. We use the standard @Mock/@InjectMocks annotations to create our mock and the object we are testing. The drawback here is that we must remember to call postInit ourselves:

@ExtendWith(MockitoExtension.class)
class MyServiceTest {
    private static final Integer ID = 42;

    @Mock
    MyRepository myRepository;

    @InjectMocks
    MyService myService;

    @Test
    public void myTest() {
        // Given.
        MyObject myObj = new MyObject();
        myObj.setId(ID);

        Mockito.when(myRepository.findByName(anyString())).thenReturn(myObj);
        myService.postInit();
        // Test runs here.
        assertThat(myService.myMethod()).isEqualTo(ID);
    }
}

To use Spring to call postInit you can do this: (I'm not sure if there's a more idiomatic way to create the mock MyRepository instance)

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = {MyService.class})
@ContextConfiguration(classes = {MyServiceTest.Configuration.class})
class MyServiceTest {
    @TestConfiguration
    static class Configuration {
        @Bean
        public MyRepository myRepository() {
            MyRepository myRepository = mock(MyRepository.class);
            MyObject myObj = new MyObject();
            myObj.setId(ID);
            Mockito.when(myRepository.findByName(anyString())).thenReturn(myObj);
            return myRepository;
        }
    }
    private static final Integer ID = 42;
    
    @Autowired
    MyService myService;
    
    @Test
    public void myTest() {
        assertThat(myService.myMethod()).isEqualTo(ID);
    }
}

One way to avoid using @SpringBootTest is to create the MyService instance in our test configuration (I've converted it to constructor injection too):

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {MyServiceTest.Configuration.class})
class MyServiceTest {
    @TestConfiguration
    static class Configuration {
        @Bean
        public MyRepository myRepository() {
            MyRepository myRepository = mock(MyRepository.class);
            MyObject myObj = new MyObject();
            myObj.setId(ID);
            Mockito.when(myRepository.findByName(anyString())).thenReturn(myObj);
            return myRepository;
        }

        @Bean
        public MyService myService(MyRepository myRepository) {
            return new MyService(myRepository);
        }
    }
    private static final Integer ID = 42;

    @Autowired
    MyService myService;

    @Test
    public void myTest() {
        assertThat(myService.myMethod()).isEqualTo(ID);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM