【问题标题】:Unit tests for Rest api controllerRest api 控制器的单元测试
【发布时间】:2019-04-10 14:51:51
【问题描述】:

我正在尝试使用 mockitospring 控制器 进行测试,但它不起作用。

这是我的控制器:

@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 无内容的响应!

【问题讨论】:

    标签: java spring rest spring-mvc mockito


    【解决方案1】:

    我会尽力为您提供一些可能对您有所帮助的指南:

    1. 从单元测试类文件中删除该静态列表和候选定义。它会造成混乱,因为测试应该彼此隔离,并且您可以在所有测试之间共享一个候选对象。只需通过在您的测试类中创建一个静态 getATestCandidate() 方法来纠正此问题,该方法每次都会为您提供一个新的 Candidate()。 (检查 Java 中的静态成员与静态方法)如果您稍后看到您有其他需要候选者的测试类,请将此方法移动到单独的 Util 类中并从不同的测试中调用它,或者更好地为您的候选者创建一个 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)以在通过 id 调用时返回候选者。第一行很好,但调用它并确保 id 为 1 是无用的,因为您指示模拟返回该候选者,现在您测试它是否返回它?(您应该相信 Mockito 确实如此)=> 删除第 2、3 行和 findByIdOk() 中的 4 个。

    对 findByIdOk() 测试方法的另一个改进是使用 Mock MVC fluent API 来检查您的状态和响应内容。

    所以你的 find by 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 在您已经指示它返回一个 id 为 1 的候选者(这并不能证明任何事情)和测试您的控制器单元能够返回一个候选者之间的区别当查询特定候选 ID 时,资源表示为 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();
    }
    

    现在您需要进行一个测试,以检查您的控制器在调用具有不存在的候选 id 的删除端点时是否返回 Not found。为此,您将在测试中指示模拟协作者 (candidateService) 在调用该 ID 时返回 null。不要陷入在您的模拟候选服务上再次执行任何断言的陷阱。此测试的目标是确保您的端点在使用不存在的候选 ID 调用时返回 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”)。

    【讨论】:

      【解决方案2】:

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

      【讨论】:

        【解决方案3】:

        感谢您的回复 :) 现在我将控制器更改为:

        @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() 抛出异常 {

            Candidate candidate = getATestCandidate();
        
            doNothing().when(candidateService).deleteById(anyInt());
        
            mockMvc.perform(
                    delete("/candidates/{id}", candidate.getId())
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isNoContent());
        
        }
        

        【讨论】:

        • 谢谢! @IoanM 我也编辑了我的最后一个答案,一切都很好,期待一个小问题:)
        • 请检查我的删除测试编辑。当您想从控制器返回 404 时,您应该指示 CandidateService 抛出 ResourceNotFoundException
        • 是的先生,我添加了 try {candidateService.deleteById(id) } catch (ResourceNotFoundException e) { .. 到我的控制器,但我不知道如何在测试中检查它,因为它正在做空:doNothing ().when(candidateService).deleteById(anyInt());对吗?
        • 请阅读我帖子的结尾,因为我在看到您的问题后对其进行了编辑。 when(candidateService.deleteById(anyInt()))).thenThrow(new ResourceNotFoundException());这就是您设置服务以引发异常然后检查未找到的方式..
        • 我不知道我什么时候尝试 thenThrow 但它不起作用,反正我是这样做的 doThrow(new ResourceNotFoundException(candidate.getId())).when(candidateService).deleteById(anyInt()) ;并且工作正常感谢您的时间,再次感谢您
        猜你喜欢
        • 1970-01-01
        • 2019-04-18
        • 2016-01-31
        • 2023-03-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多