Модульные тесты для контроллера Rest API - PullRequest
0 голосов
/ 10 апреля 2019

Я пытаюсь выполнить тест для пружинного контроллера с mockito , но он не работает.

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

@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("test@ALEX.com");
        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\":\"test@ALEX.com\",\"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 не найден, но я получаю ответ с 204 без содержимого!

Ответы [ 3 ]

1 голос
/ 10 апреля 2019

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

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

  2. С тестовой средой Spring MVC у вас есть возможность проверить всю инфраструктуру конечных точек, включая коды состояния HTTP, сериализацию ввода и вывода, тело ответа, перенаправления и т. Д. Не отклоняйтесь от этого, проверяя несущественные вещи: В первой части теста findByIdOk () вы тестируете свой собственный макет.

 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. Это должно быть частью упорядочения теста, где вы настраиваете вашего сотрудника-контроллера (андидат-служба) для возврата кандидата при вызове по идентификатору. Первая строка в порядке, но вызывать ее и проверять, что id равен 1, бесполезно, потому что вы дали команду макету вернуть этого кандидата, а теперь вы проверяете, что он его возвращает? (Вы должны доверять Mockito, что он это делает) => Удалить строки 2, 3 и 4 из findByIdOk ().

Еще одним усовершенствованием метода теста findByIdOk () было бы использование свободно используемого API Mock MVC для проверки вашего статуса и содержания ответов.

Таким образом, ваш метод поиска по идентификатору может стать (проверьте пункт 3, чтобы понять, почему я переименовал идентификатор):

@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 вместо создания теста

@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 при вызове конечной точки удаления с несуществующим идентификатором кандидата.Для этого вы проинструктируете в своем тесте фиктивного соавтора (candidService) возвращать ноль при вызове этого идентификатора.Не попадайтесь в ловушку повторного выполнения каких-либо утверждений на вашем поддельном сервисе кандидата.Цель этого теста - убедиться, что ваша конечная точка возвращает 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
}

Пожалуйста, адаптируйте свои тесты для полученияконечные точки, чтобы также проверить тело JSON.Наличие теста, который проверяет только состояние, когда конечная точка возвращает также некоторое тело ответа, завершено только наполовину.

Пожалуйста, ознакомьтесь с документацией о том, как структурировать конечные точки.То, что вы сделали здесь, возможно, работает и компилируется, но это не значит, что это правильно.Я ссылаюсь на это ("/ кандидаты / имя / {имя}", "/ кандидаты / создать").

0 голосов
/ 11 апреля 2019

Спасибо за ваши ответы :) Теперь я изменил свой контроллер на:

@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 ..


@ Проверка открытого void shouldReturnNoContentWhenDeleteCandidateDontExist () создает исключение {

    Candidate candidate = getATestCandidate();

    doNothing().when(candidateService).deleteById(anyInt());

    mockMvc.perform(
            delete("/candidates/{id}", candidate.getId())
                    .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isNoContent());

}
0 голосов
/ 10 апреля 2019

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...