Модульные тесты для API-контроллера Rest

Я пытаюсь проверить пружинный контроллер с помощью Мокито, но это не работает.

Это мой контроллер:

@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 без содержания!

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
0
2 587
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

ResponseEntity.noContent() возвращает код 204, поэтому, если вы хотите, чтобы ваш контроллер возвращал 404, вам следует изменить класс контроллера, чтобы он возвращал ResponseEntity.notFound()

Ответ принят как подходящий

Я постараюсь дать вам несколько рекомендаций, которые могут вам помочь:

  1. Удалите этот статический список и определение кандидата из файла класса модульного теста. Это создает путаницу, потому что тесты должны быть изолированы друг от друга, и при этом у вас есть объект-кандидат, общий для всех тестов. Просто исправьте это, создав статический метод getATestCandidate() в вашем тестовом классе, который каждый раз дает вам новый Candidate(). (Проверьте статические члены и статические методы в Java). Если позже вы обнаружите, что у вас есть другие тестовые классы, которым нужен кандидат, переместите этот метод в отдельный класс Util и вызовите его из разных тестов или, что еще лучше, создайте класс Builder для вашего кандидата. (Проверьте шаблон проектирования Builder).

  2. С тестовой средой 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 со всеми полями-кандидатами внутри него при запросе определенного идентификатора кандидата.

  1. Поскольку у вас, вероятно, будет несколько методов тестирования для одной и той же конечной точки контроллера, назовите свои методы тестирования наводящим образом, чтобы объяснить, что именно вы пытаетесь протестировать. Таким образом, вы документируете свои тесты, и их также можно будет поддерживать. Позже кому-то другому будет очень легко выяснить, что должен делать тест и как его исправить, если он сломается. Хорошей практикой является даже наличие соглашения об именах во всем приложении.

Например, В вашем конкретном тестовом классе вместо создания теста

@Test
public void findAll() {
...
}

создайте один с более наводящим на размышления именем, которое также включает ресурс, которым вы манипулируете

@Test
public void shouldGetCandidatesList() {
...
}

или

@Test
public void shouldReturn404NotFoundWhenGetCandidateByIdAndItDoesntExist() {
...
}
  1. Теперь перейдем к конечной точке удаления и реализации службы. Вы можете поместить вызов service.deleteById() в блок try catch, поймать исключение ResourceNotFound и вернуть с вашего контроллера 404.

Ваша служба удаления может выглядеть так, потому что вы знаете, что 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());

}

Спасибо ! @IoanM Я тоже отредактировал свой последний ответ, все в порядке, ждите небольшой проблемы :)

sesh-777 11.04.2019 14:17

пожалуйста, проверьте мое редактирование для теста на удаление. Вы должны поручить кандидату в службу генерировать исключение ResourceNotFoundException, когда вы хотите вернуть 404 из вашего контроллера.

Ioan M 11.04.2019 16:03

да, сэр, я добавил try {candidateService.deleteById(id) } catch (ResourceNotFoundException e) { .. в свой контроллер, но я не знаю, как проверить это в тесте, потому что я делаю пустоту: doNothing().when(candidateService). удалитьById(любоеInt()); это правильно ?

sesh-777 11.04.2019 17:11

пожалуйста, прочитайте конец моего поста, потому что я отредактировал его после того, как увидел, в чем ваша проблема. когда(candidateService.deleteById(anyInt()))).thenThrow(new ResourceNotFoundException()); вот как вы настраиваете службу для создания исключения, а затем проверяете, не найдено ли это.

Ioan M 11.04.2019 17:56

я не знаю, я пытаюсь, когда thenThrow, но это не работает, во всяком случае, я сделал это так: doThrow(new ResourceNotFoundException(candidate.getId())).when(candidate‌​Service).deleteById(‌​anyInt()); и он работает нормально, спасибо за ваше время и еще раз большое спасибо

sesh-777 12.04.2019 09:32

по крайней мере, вы поняли идею ... поскольку я писал это не в редакторе, который может сказать мне, компилируется ли он или нет, я могу ошибаться с синтаксисом, но вы поняли идею. И если мой ответ был полезен для вас, я снова отредактирую свой ответ, чтобы исправить синтаксис, и если он был вам полезен, пожалуйста, отметьте его соответствующим образом :)

Ioan M 12.04.2019 09:42

Другие вопросы по теме