Reputation: 41
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());
}
}
Upvotes: 0
Views: 6204
Reputation: 41
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));
}
Upvotes: 0
Reputation: 42491
You mix here many concepts:
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.
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
Upvotes: 2
Reputation: 12292
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.
Upvotes: 0
Reputation: 3589
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
.
Upvotes: 2