Я пытаюсь проверить пружинный контроллер с помощью Мокито, но это не работает.
Это мой контроллер:
@RestController
public class CandidateController {
private static final Logger log = LoggerFactory.getLogger(CandidateController.class);
private CandidateService candidateService;
@Autowired
public CandidateController(CandidateService candidateService) {
this.candidateService = candidateService;
}
@GetMapping("/candidates")
public ResponseEntity<List<Candidate>> getAllCandidates() {
List<Candidate> candidates = candidateService.findAll();
log.info("Candidates list size = {}", candidates.size());
if (candidates.size() == 0) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.ok(candidates);
}
@GetMapping("/candidates/{id}")
public ResponseEntity<Candidate> getCandidateById(@PathVariable int id) {
Candidate candidate = candidateService.findById(id);
if (candidate != null) {
return ResponseEntity.ok(candidate);
} else {
log.info("Candidate with id = {} not found", id);
return ResponseEntity.notFound().build();
}
}
@GetMapping("/candidates/name/{name}")
public ResponseEntity<List<Candidate>> getCandidatesWhereNameLike(@PathVariable String name) {
List<Candidate> candidates = candidateService.findByLastNameLike("%" + name + "%");
log.info("Candidates by name list size = {}", candidates.size());
if (candidates.isEmpty()) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.ok(candidates);
}
@PostMapping("/candidates/create")
public ResponseEntity<Object> postCandidate(@Valid @RequestBody Candidate candidate) {
Candidate newCandidate = candidateService.save(candidate);
if (newCandidate != null) {
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(newCandidate.getId())
.toUri();
return ResponseEntity.created(location).build();
} else {
log.info("Candidate is already existing or null");
return ResponseEntity.unprocessableEntity().build();
}
}
@PutMapping("/candidates/{id}")
public ResponseEntity<Object> updateCandidate(@PathVariable int id, @RequestBody Candidate candidate) {
candidateService.update(candidate, id);
candidate.setId(id);
return ResponseEntity.noContent().build();
}
@DeleteMapping("/candidates/{id}")
public ResponseEntity<Void> deleteCandidate(@PathVariable int id) {
candidateService.deleteById(id);
return ResponseEntity.noContent().build();
}
Это моя служба:
@Service
public class CandidateServiceImpl implements CandidateService {
private CandidateRepository candidateRepository;
private static final Logger log = LoggerFactory.getLogger(CandidateServiceImpl.class);
public CandidateServiceImpl() {
}
@Autowired
public CandidateServiceImpl(CandidateRepository repository) {
this.candidateRepository = repository;
}
@Override
public List<Candidate> findAll() {
List<Candidate> list = new ArrayList<>();
candidateRepository.findAll().forEach(e -> list.add(e));
return list;
}
@Override
public Candidate findById(int id) {
Candidate candidate = candidateRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(id));
return candidate;
}
@Override
public Candidate findBySocialNumber(int number) {
Candidate candidate = candidateRepository.findBySocialNumber(number).orElse(null);
return candidate;
}
@Override
public List<Candidate> findByLastNameLike(String userName) {
return candidateRepository.findByLastNameLike(userName).orElseThrow(() -> new ResourceNotFoundException(0, "No result matches candidates with name like : " + userName));
}
@Override
public Candidate save(Candidate candidate) {
Candidate duplicateCandidate = this.findBySocialNumber(candidate.getSocialNumber());
if (duplicateCandidate != null) { // Candidat existant avec numéro sécuAucun Candidat avec ce numéro sécu
log.info("Candidate with username = {} found in database", candidate.getSocialNumber());
throw new ResourceAlreadyExistException("Social security number : " + (candidate.getSocialNumber()));
}
log.info("Candidate with social number = {} found in database", candidate.getSocialNumber());
return candidateRepository.save(candidate);
}
@Override
public void update(Candidate candidate, int id) {
log.info("Candidate to be updated : id = {}", candidate.getId());
Candidate candidateFromDb = this.findById(id);
if (candidateFromDb != null) {
// Candidate présent => update
candidate.setId(id);
candidateRepository.save(candidate);
} else {
// Candidate absent => no update
log.info("Candidate with id = {} cannot found in the database", candidate.getId());
throw new ResourceNotFoundException(id);
}
}
@Override
public void deleteById(int id) {
Candidate candidate = this.findById(id);
if (candidate != null) {
candidateRepository.delete(candidate);
} else {
throw new ResourceNotFoundException(id);
}
}
}
Мой тестовый файл:
@RunWith(SpringRunner.class)
@WebMvcTest(value = CandidateController.class, secure = false)
public class CandidateControllerTestMockito {
//parse date to use it in filling Candidate model
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
String dateString = format.format(new Date());
Date date = format.parse("2009-12-31");
static private List<Candidate> candidates = new ArrayList<>();
static Candidate candidate = new Candidate();
{
candidate.setId(1);
candidate.setLastName("pierre");
candidate.setFirstName("pust");
candidate.setBirthDate(date);
candidate.setNationality("testFrancaise");
candidate.setBirthPlace("testParis");
candidate.setBirthDepartment("test92");
candidate.setGender("testMale");
candidate.setSocialNumber(1234);
candidate.setCategory("testCategory");
candidate.setStatus("testStatus");
candidate.setGrade("testGrade");
candidate.setFixedSalary(500);
candidate.setPrivatePhoneNumber(0707070707);
candidate.setPrivateEmail("[email protected]");
candidate.setPosition("testPosition");
candidate.setStartingDate(date);
candidate.setSignatureDate(date);
candidate.setContractStatus("testContractStatus");
candidate.setContractEndDate("testContractEnd");
candidate.setIdBusinessManager(1);
candidate.setIdAdress(12);
candidate.setIdMissionOrder(11);
candidates.add(candidate);
}
@Autowired
private MockMvc mockMvc;
@MockBean
private CandidateService candidateService;
public CandidateControllerTestMockito() throws ParseException {
}
@Test
public void findAll() throws Exception {
when(
candidateService.findAll()).thenReturn(candidates);
RequestBuilder requestBuilder = get(
"/candidates").accept(
MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(requestBuilder).andReturn();
System.out.println("ici"+candidates.toString());
String expected = "[{\"lastName\":\"pierre\",\"firstName\":\"pust\",\"birthDate\":1262214000000,\"nationality\":\"testFrancaise\",\"birthPlace\":\"testParis\",\"birthDepartment\":\"test92\",\"gender\":\"testMale\",\"socialNumber\":1234,\"category\":\"testCategory\",\"status\":\"testStatus\",\"grade\":\"testGrade\",\"fixedSalary\":500.0,\"privatePhoneNumber\":119304647,\"privateEmail\":\"[email protected]\",\"position\":\"testPosition\",\"schoolYear\":null,\"startingDate\":1262214000000,\"signatureDate\":1262214000000,\"contractStatus\":\"testContractStatus\",\"contractEndDate\":\"testContractEnd\",\"idBusinessManager\":1,\"idAdress\":12,\"idMissionOrder\":11}]";
JSONAssert.assertEquals(expected, result.getResponse()
.getContentAsString(), false);
}
@Test
public void findByIdOk() throws Exception {
when(candidateService.findById(candidate.getId())).thenReturn(candidate);
Candidate cand=candidateService.findById(candidate.getId());
int idCand=cand.getId();
assertEquals(idCand,1);
RequestBuilder requestBuilder = get(
"/candidates/1").accept(
MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(requestBuilder).andReturn();
MockHttpServletResponse response = result.getResponse();
assertEquals(HttpStatus.OK.value(), response.getStatus());
}
@Test
public void findByIdFail() throws Exception {
when(candidateService.findById(18)).thenReturn(null);
RequestBuilder requestBuilder = get(
"/candidates/18").accept(
MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(requestBuilder).andReturn();
MockHttpServletResponse response = result.getResponse();
assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatus());
}
@Test
public void deleteCandidate() throws Exception{
when(candidateService.findById(candidate.getId())).thenReturn(candidate);
doNothing().when(candidateService).deleteById(candidate.getId());
mockMvc.perform(
delete("/candidates/{id}", candidate.getId()))
.andExpect(status().isNoContent());
}
спрашиваю правильно ли я поступаю или нет? и я хочу сделать ТЕСТ для deleteCandidateDontExist Я старался :
when(candidateService.findById(candidate.getId())).thenReturn(null);
doNothing().when(candidateService).deleteById(candidate.getId());
mockMvc.perform(...
Ожидаю ответа с 404 not found, но получаю ответ с 204 без содержания!
ResponseEntity.noContent() возвращает код 204, поэтому, если вы хотите, чтобы ваш контроллер возвращал 404, вам следует изменить класс контроллера, чтобы он возвращал ResponseEntity.notFound()
Я постараюсь дать вам несколько рекомендаций, которые могут вам помочь:
Удалите этот статический список и определение кандидата из файла класса модульного теста. Это создает путаницу, потому что тесты должны быть изолированы друг от друга, и при этом у вас есть объект-кандидат, общий для всех тестов. Просто исправьте это, создав статический метод getATestCandidate() в вашем тестовом классе, который каждый раз дает вам новый Candidate(). (Проверьте статические члены и статические методы в Java). Если позже вы обнаружите, что у вас есть другие тестовые классы, которым нужен кандидат, переместите этот метод в отдельный класс Util и вызовите его из разных тестов или, что еще лучше, создайте класс Builder для вашего кандидата. (Проверьте шаблон проектирования Builder).
С тестовой средой Spring MVC у вас есть возможность проверить всю инфраструктуру конечной точки, включая коды состояния HTTP, сериализацию ввода и вывода, тело ответа, перенаправления и т. д. Не отклоняйтесь от этого, тестируя несущественные вещи: В первой части теста findByIdOk() вы тестируете свой собственный Mock.
4. when(candidateService.findById(candidate.getId())).thenReturn(candidate);
5. Candidate cand=candidateService.findById(candidate.getId());
6. int idCand=cand.getId();
7. assertEquals(idCand,1);
Не забывайте фундаментальную концепцию модульных тестов AAA (Arrange, Act, Assert), которая также применима к тестам MVC. Это должна быть часть теста, в которой вы настраиваете соавтора контроллера (candidateService) для возврата кандидата при вызове по идентификатору. Первая строка в порядке, но вызывать ее и проверять, что идентификатор равен 1, бесполезно, потому что вы приказали макету вернуть этого кандидата, и теперь вы проверяете, что он возвращает его? (Вы должны доверять Mockito, что он делает) => Удалить строки 2, 3 и 4 из findByIdOk().
Другим улучшением тестового метода findByIdOk() будет использование API-интерфейса Mock MVC Fluent для проверки вашего статуса и содержимого ответа.
Таким образом, ваш поиск по методу id может стать (проверьте пункт 3, чтобы понять, почему я переименовал id):
@Test
public void shouldReturnCandidateById() throws Exception {
//ARRANGE
Candidate candidate = getATestCandidate();
when(candidateService.findById(candidate.getId())).thenReturn(candidate);
RequestBuilder requestBuilder = get(
"/candidates/" + candidate.getId()).accept(
MediaType.APPLICATION_JSON);
//ACT
MvcResult result = mockMvc.perform(requestBuilder).
//ASSERT
.andExpect(status().is(200))
.andExpect(jsonPath("$.id", is(candidate.getId())))
...
//here you are checking whether your controller returns the
//correct JSON body representation of your Candidate resource
//so I would do jsonPath checks for all the candidate fields
//which should be part of the response
}
Лучше проверять поля json с путем json отдельно, чем проверять все тело json целиком.
Теперь подумайте о разнице между проверкой того, что ваш фиктивный соавтор CandidateService возвращает кандидата с идентификатором 1, когда вы уже проинструктировали его сделать это (это ничего не доказывает), и проверкой того, что ваш блок контроллера может возвращать представление ресурса кандидата как JSON со всеми полями-кандидатами внутри него при запросе определенного идентификатора кандидата.
Например, В вашем конкретном тестовом классе вместо создания теста
@Test
public void findAll() {
...
}
создайте один с более наводящим на размышления именем, которое также включает ресурс, которым вы манипулируете
@Test
public void shouldGetCandidatesList() {
...
}
или
@Test
public void shouldReturn404NotFoundWhenGetCandidateByIdAndItDoesntExist() {
...
}
Ваша служба удаления может выглядеть так, потому что вы знаете, что API службы должен вызывать исключение ResourceNotFoundException, если вы пытаетесь удалить кандидата, которого не существует:
@DeleteMapping("/candidates/{id}")
public ResponseEntity<Void> deleteCandidate(@PathVariable int id) {
try{
candidateService.deleteById(id);
} catch(ResourceNotFoundException e) {
ResponseEntity.notFound().build()
}
return ResponseEntity.noContent().build();
}
Теперь вам нужно выполнить тест, который проверяет, что ваш контроллер возвращает Not found при вызове конечной точки удаления с несуществующим идентификатором кандидата. Для этого вы проинструктируете в своем тесте фиктивного сотрудника (candidateService) возвращать значение null при вызове для этого идентификатора. Не попадайтесь в ловушку повторного выполнения каких-либо утверждений на вашем фиктивном CanditService. Цель этого теста — убедиться, что ваша конечная точка возвращает NotFound при вызове с несуществующим идентификатором кандидата.
Скелет теста shouldReturnNotFoundWhenGetCandidateByNonExistingId()
@Test
public void shouldReturnNotFoundWhenGetCandidateByNonExistingId() {
//the Arrange part in your test
doThrow(new ResourceNotFoundException(candidate.getId())).when(candidateService).deleteById(anyInt());
//call mockMvc
//assert not found using the MockMvcResultMatchers
}
Пожалуйста, адаптируйте свои тесты для конечных точек get, чтобы они также проверяли тело JSON. Наличие теста, который проверяет только состояние, когда конечная точка возвращает также некоторое тело ответа, завершено только наполовину.
Пожалуйста, также ознакомьтесь с документацией о том, как структурировать ваши конечные точки. То, что вы сделали здесь, вероятно, работает и компилируется, но это не значит, что это правильно. Я имею в виду это ("/candidates/name/{name}", "/candidates/create").
Спасибо за ваши ответы :) теперь я изменил свой контроллер на:
@DeleteMapping("/candidates/{id}")
public ResponseEntity<Void> deleteCandidate(@PathVariable int id) {
try {
candidateService.deleteById(id);
} catch (ResourceNotFoundException e) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
мой тест на удаление работает нормально:
@Test
public void shouldDeleteCandidate() throws Exception {
Candidate candidate = getATestCandidate();
doNothing().when(candidateService).deleteById(candidate.getId());
mockMvc.perform(
delete("/candidates/{id}", candidate.getId())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNoContent());
}
но shouldReturn404WhenDeleteCandidateDontExist не возвращает никакого контента, и я ожидал 404 ..
@Тестовое задание public void shouldReturnNoContentWhenDeleteCandidateDontExist() выдает Exception {
Candidate candidate = getATestCandidate();
doNothing().when(candidateService).deleteById(anyInt());
mockMvc.perform(
delete("/candidates/{id}", candidate.getId())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNoContent());
}
пожалуйста, проверьте мое редактирование для теста на удаление. Вы должны поручить кандидату в службу генерировать исключение ResourceNotFoundException, когда вы хотите вернуть 404 из вашего контроллера.
да, сэр, я добавил try {candidateService.deleteById(id) } catch (ResourceNotFoundException e) { .. в свой контроллер, но я не знаю, как проверить это в тесте, потому что я делаю пустоту: doNothing().when(candidateService). удалитьById(любоеInt()); это правильно ?
пожалуйста, прочитайте конец моего поста, потому что я отредактировал его после того, как увидел, в чем ваша проблема. когда(candidateService.deleteById(anyInt()))).thenThrow(new ResourceNotFoundException()); вот как вы настраиваете службу для создания исключения, а затем проверяете, не найдено ли это.
я не знаю, я пытаюсь, когда thenThrow, но это не работает, во всяком случае, я сделал это так: doThrow(new ResourceNotFoundException(candidate.getId())).when(candidateService).deleteById(anyInt()); и он работает нормально, спасибо за ваше время и еще раз большое спасибо
по крайней мере, вы поняли идею ... поскольку я писал это не в редакторе, который может сказать мне, компилируется ли он или нет, я могу ошибаться с синтаксисом, но вы поняли идею. И если мой ответ был полезен для вас, я снова отредактирую свой ответ, чтобы исправить синтаксис, и если он был вам полезен, пожалуйста, отметьте его соответствующим образом :)
Спасибо ! @IoanM Я тоже отредактировал свой последний ответ, все в порядке, ждите небольшой проблемы :)