简体   繁体   中英

Spring Boot unit test won't run. NullInsteadOfMockException

I've been following JavaWorld's JUnit 5 Guide to write my tests but the tests won't run. The exception is NullInsteadOfMockException. Can anyone help me figure out what I'm doing wrong? JavaWorld's guide is at https://www.javaworld.com/article/3537563/junit-5-tutorial-part-1-unit-testing-with-junit-5-mockito-and-hamcrest.html

Error message:

org.mockito.exceptions.misusing.NullInsteadOfMockException: 
Argument passed to when() is null!
Example of correct stubbing:
    doThrow(new RuntimeException()).when(mock).someMethod();
Also, if you use @Mock annotation don't miss initMocks()

Test class:

@ExtendWith(MockitoExtension.class)
class ConferenceServiceTest {

    @Autowired
    ConferenceServiceImpl conferenceService;

    @Mock
    ConferenceRepository conferenceRepository;

    @Mock
    ConferenceRoomRepository conferenceRoomRepository;

    Conference conference;
    ConferenceRoom conferenceRoom;
    final Integer MAX_CAPACITY = 5;

    @BeforeEach
    void setUp() {
        LocalDateTime conferenceStartDateTime = LocalDateTime.of(2020, Month.JUNE, 20, 10, 15);
        LocalDateTime conferenceEndDateTime = LocalDateTime.of(2020, Month.JUNE, 20, 11, 15);
        conference = new Conference("conferenceName", conferenceStartDateTime, conferenceEndDateTime);
        conferenceRoom = new ConferenceRoom("testRoomName", "testRoomLocation", MAX_CAPACITY);
        conference.setConferenceRoom(conferenceRoom);
    }

    @Test
    void addConference_alreadyExists() {
        doReturn(conference).when(conferenceService).findConference(conference);

        assertThrows(ConferenceAlreadyExistsException.class, () -> conferenceService.addConference(conference));
    }
}

JUnit and Mockito part of my pom.xml

 <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>3.3.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.6.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

Service class code

@Service
@Slf4j
public class ConferenceServiceImpl implements ConferenceService {

    private ConferenceRepository conferenceRepository;
    private ConferenceRoomRepository conferenceRoomRepository;

    public ConferenceServiceImpl(ConferenceRepository conferenceRepository, ConferenceRoomRepository conferenceRoomRepository) {
        this.conferenceRepository = conferenceRepository;
        this.conferenceRoomRepository = conferenceRoomRepository;
    }

    public String addConference(Conference conference) {

        throw new RuntimeException("Not yet implemented");
    }

    public String cancelConference(Conference conference) {

        throw new RuntimeException("Not yet implemented");
    }

    public String checkConferenceRoomAvailability(Conference conference) {

        throw new RuntimeException("Not yet implemented");
    }

    public Conference findConference(Conference conference) {
        return conferenceRepository.findConferenceByNameAndStartDateAndTimeAndEndDateAndTime(
                conference.getName(), conference.getStartDateAndTime(), conference.getEndDateAndTime());
    }

    public ConferenceRoom findConferenceRoom(ConferenceRoom conferenceRoom) {
        return conferenceRoomRepository.findConferenceRoomByNameAndAndLocation(
                conferenceRoom.getName(), conferenceRoom.getLocation());
    }

}

Instead of

@Autowired
ConferenceServiceImpl conferenceService;

you need to use

@InjectMocks
ConferenceServiceImpl conferenceService;

Because, when you Autowire a bean, it gets created with the dependencies from the spring container. In the interest of this test, you want it to be created with mocks which are defined by @Mock .

You mix here many concepts:

  1. Use @Autowire only in spring ecosystem (real code or test driven by spring). Here you don't have spring in the test, therefor don't use it.

  2. In a regular unit test you better create the subject (class that you're about to test) by yourself.


@ExtendWith(MockitoExtension.class)
class ConferenceServiceTest {

    // Note, its not a mock, its not autowired!
    ConferenceServiceImpl conferenceService;

    @Mock
    ConferenceRepository conferenceRepository;

    @Mock
    ConferenceRoomRepository conferenceRoomRepository;

    ....

    @BeforeEach
    void setUp() {
      ... // the code that you already have
      ...
      conferenceService = new ConferenceServiceImpl(conferenceRepository, conferenceRoomRepository);
    }

IMO make sure that this setup work for you before learning advanced stuff like @InjectMocks

You autowired conferenceService so it is not a mock that you could adjust in its bravhior. Either use a mock or a spy (partial mock).

Because it is also the unit under test you still need to inject the other mocks into the spy.

Try this:

import static org.mockito.Mockito.spy;

@ExtendWith(MockitoExtension.class)
class ConferenceServiceTest {

    @InjectMocks
    ConferenceServiceImpl conferenceService;

    @Mock
    ConferenceRepository conferenceRepository;

    @Mock
    ConferenceRoomRepository conferenceRoomRepository;

    // ... other stuff

    @Test
    void addConference_alreadyExists() {
        // partial mock
        ConferenceServiceImpl spy = spy(conferenceService);

        // mock a specific method of the spy
        doReturn(conference).when(spy).findConference(conference);

        // use the partial mock / spy to call the real method under test that uses the mocked other method.
        assertThrows(ConferenceAlreadyExistsException.class, () -> spy.addConference(conference));
    }
}

Remember to always use the spy and not calling the original non-spyed object when trying to setup a partial mock!

Also note that such partial mocks are a hint for a non-ideal architecture and potentially indicate mixed responisbilities. So maybe consider extracting the different methods to their own class and gain easier testability. Also remember that tests and code in general are much more often read than written and partial mocks are harder to comprehend in 6 month when you try to figure out what the heck you did try to setup as test case.

Stuck's answer seems to be the solution. "you try to mock behavior of the unit under test (findConference of the service itself). In the guide they only mock behavior of a different unit (the repository instead of the service). While the repository is a mock, the service is not."

The code runs if I use

@Mock
ConferenceRepository conferenceRepository;

@Mock
ConferenceRoomRepository conferenceRoomRepository;

@InjectMocks
ConferenceServiceImpl conferenceService;

and

@Test
    void addConference_alreadyExists() {
        doReturn(conference).when(conferenceRepository).findConferenceByNameAndStartDateAndTimeAndEndDateAndTime(
                conference.getName(), conference.getStartDateAndTime(), conference.getEndDateAndTime());;

        assertThrows(ConferenceAlreadyExistsException.class, () -> conferenceService.addConference(conference));
    }

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