Я пытаюсь написать модульный тест для следующего контроллера:
@RestController
@RequestMapping(value = ENDPOINT_URL)
public class MainController extends RestControllerBase {
MainService mainService;
public MainController(MainService mainService) {
this.mainService = mainService;
}
@GetMapping
public ResponseEntity<List<MainDataDto>> getDashboardData() {
checkPermission();
List<MainDataDto> result = mainService.getData();
return ResponseEntity.ok().body(result);
}
}
Вот что у меня есть на данный момент с точки зрения модульного теста:
class MainControllerTest {
MainController mainController;
@Test
public void test_getDashboardData_shouldReturn200Response() {
MainService mainService = Mockito.mock(MainService.class);
mainController = new MainController(mainService);
List<MainDataDto> mockData = List.of(new MainDataDto());
when(mainService.getData()).thenReturn(mockData);
Assertions.assertEquals(HttpStatus.OK, mainController.getDashboardData().getStatusCode());
}
Когда я запускаю этот модульный тест, я получаю следующее исключение:
java.lang.NullPointerException: Cannot invoke "org.springframework.security.core.Authentication.getPrincipal()" because the return value of "org.springframework.security.core.context.SecurityContext.getAuthentication()" is null
Это исключение исходит из checkPermission
, который является защищенным методом в классе RestControllerBase
. Я не могу изменить модификатор доступа для этого метода.
Я новичок в написании модульных тестов, поэтому стараюсь делать это понемногу, чтобы понять, что делаю. Из-за исключения я хочу «выяснить, как справиться» с checkPermission
и (с моим ограниченным пониманием) я думаю, что могу/должен как-то высмеять этот метод или что-то вроде передачи этой части моего метода контроллера над.
Поскольку этот метод защищен, как лучше всего изменить мой модульный тест, чтобы я больше не получал приведенное выше исключение?
Рассмотрите возможность использования макета MVC.
В качестве краткосрочного решения модульный тест может шпионить за MainController
, например:
mainController = Mockito.spy(new MainController(mainService));
что позволяет имитировать некоторое поведение тестируемого класса. Например:
doNothing().when(mainController).checkPermission();
Однако, поскольку часть класса, которую необходимо протестировать, больше не тестируется (поскольку ее поведение высмеивается), похоже, что класс следует перефакторизовать. Например, все, что делает checkPermission()
, можно переместить в отдельный класс сервиса/компонента и внедрить в контроллер так же, как MainService
. Это упростило бы настройку модульного теста с помощью макета нового класса, где тест подтверждал бы, что новый класс был вызван.
Похоже, вы пытаетесь протестировать контроллер весенней загрузки с помощью Mockito, но вы пытаетесь проверить пружинную безопасность (или, по крайней мере, пытаетесь обойти ее). Вы можете добиться этого, добавив в свой проект зависимость от Spring-security-test.
зависимость от maven
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>6.3.1</version>
<scope>test</scope>
</dependency>
зависимость градиента
testImplementation 'org.springframework.security:spring-security-test:6.3.1'
Как только зависимость будет добавлена в ваш проект, вы можете аннотировать свой тестовый метод с помощью @WithMockUser
. Вам также потребуется аннотировать свой тестовый класс с помощью @SpringBootTest
, но это не потребует от вас добавления какой-либо инъекции зависимостей, а просто сделает аннотацию @WithMockUser
доступной для вашего теста.
@SpringBootTest
class MainControllerTest {
MainController mainController;
@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
void test_getDashboardData_shouldReturn200Response() {
MainService mainService = Mockito.mock(MainService.class);
mainController = new MainController(mainService);
List<MainDataDto> mockData = List.of(new MainDataDto());
when(mainService.getData()).thenReturn(mockData);
Assertions.assertEquals(HttpStatus.OK, mainController.getDashboardData().getStatusCode());
}
}
Вы также можете использовать это с MockMvc, если решите пойти по этому пути, поскольку это даст вам немного больше контроля над тестированием уровня сервиса без необходимости проведения интеграционного теста. Однако он также будет отлично работать с написанным вами методом тестирования.
Защищенный метод означает, что вы все еще можете
@Override
его использовать. Но уSecurityContext
есть общедоступные методы для установки аутентификации для текущего потока, поэтому вы можете просто установить ее в своем методе@BeforeEach
и очистить в своем методе@AfterEach
— или иметь блок try-finally в своем тестовом методе. См. связанный дубликат для первого подхода.