简体   繁体   中英

Unit tests for Rest api controller

I'm trying to do a test for a spring controller with mockito but it doesn't work.

This is my controller :

@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();
    }

This is my Service:


@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);
        }
    }
}

My test File:

@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());

    }



am asking if i'm doing in the right way or no ? and i want to do a TEST for deleteCandidateDontExist i tried :

when(candidateService.findById(candidate.getId())).thenReturn(null);
        doNothing().when(candidateService).deleteById(candidate.getId());
 mockMvc.perform(...


Am expecting a response with 404 not found but I get a response with 204 no content!

I will try to give you some guidelines which might help you:

  1. Remove that static list and candidate definition from the unit test class file. It creates confusion because the tests should be isolated from each other and with that you have a candidate object shared between all the tests. Simply correct this by creating a static getATestCandidate() method into your test class which is giving you a new Candidate() every time. (Check the static members vs static methods in Java) If you see later that you have other test classes which need a Candidate move this method into a separate Util class and call it from different tests or even better create a Builder class for your Candidate. (Check Builder Design Pattern).

  2. With the Spring MVC test framework you have the possibility to check the whole endpoint infrastructure including HTTP status codes, input and output serialization, response body, redirects etc. Do not deviate from that by testing irrelevant things: In the first part of the findByIdOk() test you are testing your own 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);

Don't forget the fundamental AAA concept of unit tests (Arrange, Act, Assert) which also applies to MVC tests. This should be the arrange part of the test where you're setting up your controller collaborator(candidateService) to return a candidate when called by id. First line is fine but calling it and making sure the id is 1 is useless because you instructed the mock to return that candidate and now you test that it's returning it?(you should trust Mockito that it does) => Remove lines 2, 3 and 4 from findByIdOk().

Another improvement to the findByIdOk() test method would be to use the Mock MVC fluent API for checking your status and response content.

So your find by id method could become(check point 3 to see why I renamed 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

}

Prefer checking the json fields with json path separately than checking the whole json body as a whole.

Now think about the the difference between testing that your mock collaborator CandidateService is returning a candidate with id 1 when you already instructed it to do so(this was not proving anything) and testing that your controller unit is able to return a Candidate resource representation as JSON with all the candidate fields inside it when queried for a specific candidate id.

  1. Because you will probably have multiple test methods for the same controller endpoint name your test methods in a suggestive way to explain what exactly you're trying to test. This way you document your tests and they will also become maintainable. It will be really easy for someone else later to figure out what should the test do and how to fix it if it gets broken. It's even a good practice to have a naming convention throughout your whole application.

For eg In your specific Test class instead of creating a test

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

create one with a more suggestive name which is also including the resource you're manipulating

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

or

@Test
public void shouldReturn404NotFoundWhenGetCandidateByIdAndItDoesntExist() {
...
}
  1. Now coming to the delete endpoint and the service implementation. You can put the call to service.deleteById() inside a try catch block, catch the ResourceNotFound exception and return from your controller the 404.

Your delete service can look like this because you know that the API of the service should throw a ResourceNotFoundException if you try to delete a candidate which doesn't exist:

@DeleteMapping("/candidates/{id}")
public ResponseEntity<Void> deleteCandidate(@PathVariable int id) {
    try{
        candidateService.deleteById(id);
    } catch(ResourceNotFoundException e) {
       ResponseEntity.notFound().build()
    }
    return ResponseEntity.noContent().build();
}

Now you need to do a test which checks that your controller returns Not found when calling the delete endpoint with a non existing candidate id. For this you will instruct in your test the mock collaborator (candidateService) to return null when called for that id. Don't fall into the trap of doing again any asserts on your mock candidateService. The goal of this test is to make sure your endpoint returns NotFound when called with a non existing candidate id.

Your shouldReturnNotFoundWhenGetCandidateByNonExistingId() test skeleton

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

Please adapt your tests for the get endpoints to also check for the JSON body. Having a test which is testing just the status when the endpoint returns also some response body is only half completed.

Please also check some documentation on how to structure your endpoints. What you did here is probably working and compiles but that doesn't mean it is right. I refer to this ("/candidates/name/{name}", "/candidates/create").

ResponseEntity.noContent() 返回 204 代码,因此如果您希望您的控制器返回 404,您应该更改您的控制器类以返回 ResponseEntity.notFound()

Thank you for your responses :) now i changed my controller to :

@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();

}

my delete test working fine :


@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());
}

but shouldReturn404WhenDeleteCandidateDontExist returning no content and i was expecting 404 ..


@Test public void shouldReturnNoContentWhenDeleteCandidateDontExist() throws Exception {

    Candidate candidate = getATestCandidate();

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

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

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM