Issue
I am using the "RunWith(MockitoJUnitRunner.class)", the @Mock notationt to mock the service and the @InjectMocks notation to inject the mock service to the controller. I get a NullPointerException in the ChargingStationsControllerTest:40, in the "when". I debugged and realized that the mocks are null. Here is my code:
package com.example.assignmentchargingstations.controllers;
import com.example.assignment.chargingStations.controllers.ChargingStationsController;
import com.example.assignment.chargingStations.models.AddressInfo;
import com.example.assignment.chargingStations.models.ChargingStation;
import com.example.assignment.chargingStations.models.StatusType;
import com.example.assignment.chargingStations.services.OpenChargeMapService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class ChargingStationsControllerTest {
@Mock
private OpenChargeMapService openChargeMapService;
@InjectMocks
private ChargingStationsController chargingStationsController;
@Test
public void testGetNearestChargingStations() throws Exception {
Double latitude = 90.0;
Double longitude = 90.0;
List<ChargingStation> chargingStationsListExpected = new ArrayList<>();
StatusType statusTypeExpected = StatusType.builder().IsOperational(true).build();
AddressInfo addressInfoExpected = AddressInfo.builder().Latitude(latitude).Longitude(longitude).Title("Test Charging Station").build();
chargingStationsListExpected.add(ChargingStation.builder().StatusType(statusTypeExpected).AddressInfo(addressInfoExpected).build());
when(openChargeMapService.getNearestChargingStations(anyDouble(), anyDouble())).thenReturn(chargingStationsListExpected);
ResponseEntity<List<ChargingStation>> responseExpected = chargingStationsController.getNearestChargingStations(latitude, longitude);
Assertions.assertEquals(responseExpected.getStatusCode(), HttpStatus.OK);
Assertions.assertEquals(responseExpected.getBody(), chargingStationsListExpected);
}
}
The controller that I'm testing:
package com.example.assignment.chargingStations.controllers;
import com.example.assignment.chargingStations.models.ChargingStation;
import com.example.assignment.chargingStations.services.OpenChargeMapService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Controller
public class ChargingStationsController {
private final OpenChargeMapService openChargeMapService;
public ChargingStationsController(OpenChargeMapService openChargeMapService) {
this.openChargeMapService = openChargeMapService;
}
@RequestMapping(path = "/nearest-charging-stations", method = RequestMethod.GET)
public ResponseEntity<List<ChargingStation>> getNearestChargingStations(@RequestParam Double latitude, @RequestParam Double longitude) {
List<ChargingStation> nearestChargingStations = openChargeMapService.getNearestChargingStations(latitude, longitude);
return new ResponseEntity<>(nearestChargingStations, HttpStatus.OK);
}
}
The service:
package com.example.assignment.chargingStations.services;
import com.example.assignment.chargingStations.clients.OpenChargeMapClient;
import com.example.assignment.chargingStations.models.ChargingStation;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Service
public class OpenChargeMapService {
private static final Double MAX_LATITUDE = 90.0000000;
private static final Double MIN_LATITUDE = -90.0000000;
private static final Double MAX_LONGITUDE = 180.0000000;
private static final Double MIN_LONGITUDE = -180.0000000;
private final OpenChargeMapClient openChargeMapClient;
public OpenChargeMapService(OpenChargeMapClient openChargeMapClient) {
this.openChargeMapClient = openChargeMapClient;
}
public List<ChargingStation> getNearestChargingStations(Double latitude, Double longitude) {
validateLatitudeLongitude(latitude, longitude);
List<ChargingStation> chargingStationsList = openChargeMapClient.getNearestChargingStations(latitude, longitude);
return chargingStationsList;
}
private void validateLatitudeLongitude(Double latitude, Double longitude) {
if (isLatitudeOutOfRange(latitude) || isLongitudeOutOfRange(longitude)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Values for latitude and longitude are out of range.");
}
}
private boolean isLatitudeOutOfRange(Double latitude) {
return (latitude.compareTo(MIN_LATITUDE) < 0) || (latitude.compareTo(MAX_LATITUDE) > 0);
}
private boolean isLongitudeOutOfRange(Double longitude) {
return (longitude.compareTo(MIN_LONGITUDE) < 0) || (longitude.compareTo(MAX_LONGITUDE) > 0);
}
}
The build.gradle:
plugins {
id 'org.springframework.boot' version '2.5.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.projectlombok:lombok:1.18.18'
implementation 'junit:junit:4.13.1'
annotationProcessor 'org.projectlombok:lombok:1.18.18'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
Solution
Since you are using Spring Boot. You have two options. You can either test the class in isolation or you can load in the spring context.
Both approaches have their benefits and limitations.
Supply mock directly into the class in test.
public class Test {
ClassToMock service = mock(ClassToMock.class)
ClassInTest testService = new ClassInTest(service);
@Test
public void doTest() {
....
}
}
This approach, you are supplying a mock directly into your service in test. This will result in speedier runtimes, but you will not be able to do more comprehensive integration tests.
Load mock service directly into spring context
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class Test {
@MockBean
ClassToMock service;
@Autowired
ClassInTest testService;
@Test
public void doTest() {
....
}
}
This will generate the spring context and mock the service component you have created. Doing so would test the wiring of your beans and your service in test. You will also have more flexibility in case you want to mock an encapsulating service (i.e. repository layer/data layer). You should also be able to use @Mock & @InjectMock with the class annotation you are currently using.
Answered By - shinjw
Answer Checked By - Mary Flores (JavaFixing Volunteer)