Solucionado (ver solução)
Solucionado
(ver solução)
18
respostas

[Dúvida] Testar métodos Pageable

Oi! Para eu fazer os testes do método de consulta no controller que utiliza Pageable, a única forma que eu tentei criar o método, foi o exemplo abaixo. Só que está dando erro de NullPointerException no domínio que vem nulo. Como que eu faço o testes para tratamento para estes tipos de método, para casos de sucesso e erro?

    @Test
    @DisplayName("Deveria devolver código http 200, quando as informações estiverem válidas")
    @WithMockUser(username = "test-appl@teste.com", authorities = {"ROLE_ADMIN"})
    void listBeneficiaryCenario2() throws Exception {
        PageRequest paginacao = PageRequest.of(0, 10);

        String expectedName = DadosListagemBeneficiarioCreator.createDadosListagemBeneficiarioValid().nome();

        Page<DadosListagemBeneficiario> listagemBeneficiarioPage = beneficiarioController.listBeneficiary(paginacao).getBody();

        assertThat(listagemBeneficiarioPage).isNotNull();

        assertThat(listagemBeneficiarioPage.toList())
                .isNotEmpty()
                .hasSize(1);

        assertThat(listagemBeneficiarioPage.toList().get(0).getClass()).isEqualTo(expectedName);

        assertThat(listagemBeneficiarioPage).isNotNull();
    }

Stacktrace do erro:

java.lang.NullPointerException: Cannot invoke "org.springframework.http.ResponseEntity.getBody()" because the return value of "com.plano.saude.cadastro.controller.BeneficiarioController.listBeneficiary(org.springframework.data.domain.Pageable)" is null

    at com.plano.saude.cadastro.controller.BeneficiarioControllerTest.listBeneficiaryCenario2(BeneficiarioControllerTest.java:174)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
18 respostas

Olá, Alexandre, tudo bem?

Como você bem mencionou, o erro NullPointerException indica que o método listBeneficiary está retornando null, o que sugere que talvez o mock ou a configuração do contexto do teste não esteja corretamente estabelecido para retornar uma página válida.

Uma abordagem para resolver isso seria garantir que o serviço ou o componente que está sendo chamado pelo seu controller esteja devidamente mockado para retornar um objeto Page esperado durante os testes.

Deixo uma sugestão de código, para incluir um mock para o serviço que retorna os dados de beneficiário:

//Código Omitido
    // Mock do serviço que é chamado pelo controller
    when(beneficiarioService.listBeneficiary(any(Pageable.class))).thenReturn(mockPage);

    MvcResult result = mockMvc.perform(get("/caminho/para/listBeneficiary")
            .param("page", "0")
            .param("size", "10")
            .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andReturn();

    String json = result.getResponse().getContentAsString();
    Page<DadosListagemBeneficiario> resultPage = objectMapper.readValue(json, new TypeReference<Page<DadosListagemBeneficiario>>() {});

    assertThat(resultPage).isNotNull();
    assertThat(resultPage.getContent()).hasSize(1);
    assertThat(resultPage.getContent().get(0).getNome()).isEqualTo("Nome Esperado");
}

Lembre-se de certificar que você tem as dependências corretas para MockMvc e Mockito no seu projeto e que o @WebMvcTest ou @SpringBootTest esteja configurado corretamente na classe de teste.

Destaco que o código é apenas uma sugestão. Talvez seja necessário fazer algumas adaptações.

Espero ter ajudado.

Qualquer dúvida, compartilhe no fórum.

Abraços e bons estudos!

Caso este post tenha lhe ajudado, por favor, marcar como solucionado ✓. Bons Estudos!

Oi! O que seria o "mockPage", quando vc declara para retornar no mock do serviço do controller? É o retorno da consulta passada pela paginação, retornada pela classe DadosListagemBeneficiario, correto? Porque eu tentei alterar fazendo neste modelo e está dando erro de compilação, nesta linha. O que eu poderia passar, para simular a paginação do controller para simular a consulta, para que não seja considerada nula?

Insira aqui a descrição dessa imagem para ajudar na acessibilidade

// Mock do serviço que é chamado pelo Controller
when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(dadosListagemBeneficiario);

Oi Alexandre, tudo bem?

Desculpe pela demora em obter um retorno.

O mockPage é um objeto que simula o retorno de uma página de dados no teste.

No entanto, Alexandre, vamos tentar dar alguns passos atrás. Você poderia verificar se o método listBeneficiary() está retornando corretamente o ResponseEntity com o Page de DadosListagemBeneficiario? Verifique se o Page não é nulo, se a lista de beneficiários não está vazia e se os dados estão corretos.

Oi Monalisa, tudo bem! Então, o método que está na classe original foi feito dessa forma, retornando OK, pegando o que está vindo do repository. Só não estou conseguindo pegar nos testes a lista de beneficiarios, que está vindo como "null".

    @GetMapping
    public ResponseEntity<Page<DadosListagemBeneficiario>> listBeneficiary(@PageableDefault(size = 10, sort = {"nome"}) Pageable paginacao){
        var page = beneficiarioRepository.findAllByAtivoTrue(paginacao).map(DadosListagemBeneficiario::new);

        return ResponseEntity.ok(page);
    }

Oi Alexandre, anteriormente você comentou que o seguinte trecho estava dando erro de compilação:

when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(dadosListagemBeneficiario);

Esse dadosListagemBeneficiario que você passou é um Page<List<SeuObjeto> ou um List<SeuObjeto>? Como não temos a mensagem de erro aqui, eu supus que poderia ser um erro de tipagem, pois o código da Monalisa me parece certo.

Pelo que entendi, o findAllByAtivoTrue retorna um Page<List<SeuObjeto>>, então se aquele mock que você tentou passar era um List<SeuObjeto> ele daria erro de compilação.

Então, caso você tenha mesmo passado um List ali por engano, use um new PageImpl<>(listaDoSeuObjeto) para passar esse List para um Page, resultando num objeto Page<List<SeuObjeto>>, assim vai ficar de acordo com o tipo de retorno do seu método findAllByAtivoTrue , permitindo que mock ele sem dar aquele erro de compilação que você mencionou.

Ficaria assim:

when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(new PageImpl<>(dadosListagemBeneficiario));

Oi Mateus! Na verdade não é nem uma coisa, nem outra, mas sim, um <Page<**NomeMeuObjeto**>>. Mas eu acho que eu também estou entendendo errado o comentário de vcs na hora de tentar pensar em fazer o mock e está dando errado e não consegui avançar... Será que não está reconhecendo a tipagem que eu declarei na classe original? Veja só: Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Veja o código de teste que eu criei para tentar reproduzir, pegando pelo mock:

@Test
    @DisplayName("Deveria devolver código http 200, quando as informações estiverem válidas")
    @WithMockUser(username = "test-appl@teste.com", authorities = {"ROLE_ADMIN"})
    void listBeneficiaryCenario2() throws Exception {
        // Código Omitido
        // Mock do serviço que é chamado pelo Controller
        DadosListagemDocumento dadosListagemDocumento = new DadosListagemDocumento(
                1L,
                TipoDocumento.CARTEIRA_DE_TRABALHO,
                "564789",
                LocalDate.parse("2002-03-14"),
                "Teste descricao",
                LocalDate.parse("2022-03-04"),
                LocalDate.parse("2022-03-04")
        );

        Endereco endereco= new Endereco(
                "Rua",
                "Vila da Esperanca",
                "01000-000",
                "Cidade da Oportunidade",
                "TO",
                "Casa",
                "123"
        );

        DadosListagemBeneficiario dadosListagemBeneficiario = new DadosListagemBeneficiario(
                1L,
                "Nome Beneficiario",
                "1187456841",
                LocalDate.parse("2001-10-17"),
                LocalDate.parse("2022-09-25"),
                LocalDate.parse("2022-10-23"),
                Collections.singletonList(dadosListagemDocumento),
                endereco
        );

        when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(new PageImpl<>(dadosListagemBeneficiario));

        MvcResult result = mvc.perform(get("/beneficiaries")
                        .param("page", "0")
                        .param("size", "10")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

        String json = result.getResponse().getContentAsString();
        Page<DadosListagemBeneficiario> resultPage = objectMapper.readValue(json, new TypeReference<Page<DadosListagemBeneficiario>>() {});

        assertThat(resultPage).isNotNull();
        assertThat(resultPage.getContent()).hasSize(1);
        assertThat(resultPage.getContent().get(0).getNome()).isEqualTo("Nome Esperado");
    }

Parece estar no caminho, mas precisamos rever algumas coisas...

  • Eu olhei aqui o projeto desse curso e o método findAllByAtivoTrue é um método do PacienteRepository e ele retorna um Page<Paciente>
  • Então, esse seu objeto DadosListagemBeneficiario é um DTO
  • O correto então, seria em vez de passar Page<DadosListagemBeneficiario>, passar um Page<Paciente> para o método findAllByAtivoTrue

Veja só: Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Ao mockar corretamente o paciente no método findAllByAtivoTrue, o método map transforma o Page<Paciente> em Page<DadosListagemBeneficiario>. O problema no mock é um erro de tipagem, pois findAllByAtivoTrue deve retornar Page<Paciente>, mas você está retornando DadosListagemBeneficiario.

Para o when funcionar, passe um Page<Paciente> para o thenReturn. Aqui está um exemplo:

/*
 * Em vez de criar um DadosListagemBeneficiario, crie um Paciente.
 * Crie o seu paciente como for necessário para o seu teste, esse é só um exemplo simplificado.
*/
Paciente paciente = new Paciente();

// o PageImpl precisa de um List<T> para ser instanciado
Page<Paciente> pacientes = new PageImpl<>(List.of(paciente));

when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(pacientes);

Se você mais pra frente tiver problemas na chamada do objectMapper.readValue() tente trocar o Page por PageImpl, assim:

Page<DadosListagemBeneficiario> resultPage = objectMapper.readValue(json, new TypeReference<PageImpl<DadosListagemBeneficiario>>() {});

Mesmo que sua versão da API seja diferente da do curso, essas instruções devem ajudar a adaptar sua aplicação. Espero que isso te ajude de fato.

Eu fiz um novo teste e deu erro, como se não tivesse conteúdo para passar na entrada. O Paciente, que tinha sugerido para tentar, eu coloquei Beneficiario, seguindo as regras de negócio que eu criei, que tem o mesmo significado, sem problemas. O stack do erro foi este: Insira aqui a descrição dessa imagem para ajudar na acessibilidadeInsira aqui a descrição dessa imagem para ajudar na acessibilidade

    @Test
    @DisplayName("Deveria devolver código http 200, quando as informações estiverem válidas")
    @WithMockUser(username = "test-appl@teste.com", authorities = {"ROLE_ADMIN"})
    void listBeneficiaryCenario2() throws Exception {
        /*
         * Em vez de criar um DadosListagemBeneficiario, crie um Beneficiario.
         * Crie o seu beneficiario como for necessário para o seu teste, esse é só um exemplo simplificado.
         */
        Beneficiario beneficiario = new Beneficiario();

        // o PageImpl precisa de um List<T> para ser instanciado
        Page<Beneficiario> beneficiarios = new PageImpl<>(List.of(beneficiario));

        when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(beneficiarios);

        MvcResult result = mvc.perform(get("/beneficiaries")
                        .param("page", "0")
                        .param("size", "10")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

        Page<DadosListagemBeneficiario> resultPage = null;

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true);

        try {
            String json = result.getResponse().getContentAsString();
            resultPage = objectMapper.readValue(json, new TypeReference<PageImpl<DadosListagemBeneficiario>>() {});
        } catch (Exception e) {
            e.printStackTrace();
        }

        assertThat(resultPage).isNotNull();
        assertThat(resultPage.getContent()).hasSize(1);
//        assertThat(resultPage.getContent().get(0).getNome()).isEqualTo("Nome Esperado");
    }

Na última linha, eu deixei comentado, porque eu não aponta para o nome Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Eu tentei reproduzir o mesmo teste que o seu e acabei recebendo o mesmo erro que você, mas agora não lembro o que eu fiz pra resolver kkkkk

Só que eu fiz o passo-a-passo do seu caso e aqui pra mim o teste tá funcionando, veja só como eu fiz:

// criei um DTO para representar o Paciente/Beneficiario - é um DTO simples que criei só pra simular um caso igual ao seu
public record BeneficiarioDTO(String nome) {}
// criei um método no PacienteController para simular o seu método listBeneficiary
    @GetMapping("/teste")
    public ResponseEntity<Page<BeneficiarioDTO>> metodoParaTeste(@PageableDefault(size = 3) Pageable pageable) {
        return ResponseEntity.ok(repository.findAllByAtivoTrue(pageable)
                .map(t -> new BeneficiarioDTO(t.getNome())));
    }
// Criei outro DTO que será responsável por, em vez de tentar desserializar a string Json inteira de resposta do endpoint, ele vai desserializar somente o objeto "content":
@JsonIgnoreProperties(ignoreUnknown = true)
public record DadosBeneficiarios(
        
        @JsonAlias("content")
        List<BeneficiarioDTO> beneficiarios
) {}

Para ficar mais fácil de entender o porque de eu ter criado esse DTO, veja como é a resposta JSON da API:


{
  "content": [
    {
      "nome": "Paciente-san"
    }
  ],
  "pageable": "INSTANCE",
  "last": true,
  "totalPages": 1,
  "totalElements": 1,
  "size": 1,
  "number": 0,
  "sort": {
    "empty": true,
    "sorted": false,
    "unsorted": true
  },
  "numberOfElements": 1,
  "first": true,
  "empty": false
}

Aquele objeto content do Json é para o nosso backend um List<BeneficiarioDTO>. Eu parei de tentar desserializar o JSON em um Page<BeneficiarioDTO> pois percebi que desserializar um Page diretamente não é possível, sendo possível apenas indiretamente, mas além de burocrático também seria desnecessário, pois nesse teste os dado do objeto page não são do nosso interesse. Então foquei no objeto que interessa, que é o List<BeneficiarioDTO>.

Então, enfim implementei o teste:

    @Autowired
    private MockMvc mvc;
    
    @MockBean
    private PacienteRepository repository;
    
    @Test
    @DisplayName("Deveria devolver código http 200, quando as informações estiverem válidas")
    @WithMockUser
    void listBeneficiaryCenario2() throws Exception {
        // arrange
        Paciente beneficiario = Paciente.builder().nome("Paciente-san").build();
        Page<Paciente> beneficiarios = new PageImpl<>(List.of(beneficiario));
        when(repository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(beneficiarios);

        // act
        MvcResult result = mvc.perform(get("/pacientes/teste")
                        .param("page", "0")
                        .param("size", "10")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true);

        DadosBeneficiarios responseBody = null;
        try {
            String json = result.getResponse().getContentAsString();
            responseBody = objectMapper.readValue(json, new TypeReference<DadosBeneficiarios>() {});
            
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        // assert
        assertEquals(responseBody.beneficiarios().get(0).nome(), beneficiario.getNome());
        // todas as outras assertions que quiser fazer...

    }

Bom, eu testei novamente e o erro continua, esperando erro 500, ao invés do 200, que é o cenário que estou tentando testar. Insira aqui a descrição dessa imagem para ajudar na acessibilidade

Segue abaixo o trecho do código que ajustei no teste:

    @Test
    @DisplayName("Deveria devolver código http 200, quando as informações estiverem válidas")
    @WithMockUser
    void listBeneficiaryCenario2() throws Exception {
        // arrange
        Beneficiario beneficiario = Beneficiario.builder().nome("Beneficiario-san").build();
        Page<Beneficiario> beneficiarios = new PageImpl<>(List.of(beneficiario));
        when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(beneficiarios);

        // act
        MvcResult result = mvc.perform(get("/beneficiaries")
                    .param("page", "0")
                    .param("size", "10")
                    .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true);

        Page<DadosListagemBeneficiario> responseBody = null;
        try {
            String json = result.getResponse().getContentAsString();
            responseBody = objectMapper.readValue(json, new TypeReference<PageImpl<DadosListagemBeneficiario>>() {});
        } catch (Exception e) {
            e.printStackTrace();
        }

        // assert
        assertThat(responseBody).isNotNull();
        assertThat(responseBody.getContent()).hasSize(1);
        assertEquals(responseBody.getContent().get(0).nome(), beneficiario.getNome());
    }

Olá,

Então, o problema persiste pois você está tentando converter o json em um objeto Page<?>. Acabou por ser uma falha nossa indicar o objectMapper.readValue(json, new TypeReference<Page<DadosListagemBeneficiario>>() {});, pois o Jackson não vai conseguir converter esse json em um objeto Page (mesmo usando PageImpl) e vai acabar retornando null, só percebi isso quando rodei de fato, e na minha última resposta eu mencionei isso e mostrei um jeito de driblar esse problema. Pra simplificar, agora eu coloquei os dois DTO que utilizei no exemplo anterior no escopo da classe de teste e tentei aproximar ao máximo com sua aplicação, veja:

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
public class PacienteControllerTest {
    
    @Autowired
    private MockMvc mvc;
    
    @MockBean
    private PacienteRepository repository;
    
    /**
     * Este aqui é meramente ilustrativo, só está aqui para fim de compreensão do código.
     *  
     * Use o SEU DTO DadosListagemBeneficiario, aquele que é devolvido pelo seu método controller que está sendo testado.
     */
//    public record DadosListagemBeneficiario(Long id, String nome, String email, String cpf) {}
    
    /**
     * Este será usado para converter a resposta JSON numa lista de DadosListagemBeneficiario.
     * Assim você consegue validar se os dados que o servidor respondeu são os mesmos dados que você passou no mock do 
     * `findAllByAtivoTrue`. 
     */
    @JsonIgnoreProperties(ignoreUnknown = true)
    private record DadosBeneficiarios(@JsonAlias("content") List<DadosListagemBeneficiario> beneficiarios) {};
    
    
    @Test
    @DisplayName("Deveria devolver código http 200, quando as informações estiverem válidas")
    @WithMockUser
    void listBeneficiaryCenario01() throws Exception {
        // arrange
        
        /**
         * Usei as anotações @Builder e @AllArgsConstructor do Lombok na classe Paciente(Beneficiario),
         * assim ele gera um método estático que retorna um builder do objeto.
         */
        Paciente beneficiario = Paciente.builder()
                .id(1L)
                .nome("Beneficiario-san")
                .email("seila@email.com")
                .cpf("927.326.610-07")
                .build();
        
        Page<Paciente> beneficiarios = new PageImpl<>(List.of(beneficiario));
        when(repository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(beneficiarios);

        // act
        MvcResult result = mvc.perform(get("/pacientes/teste/v2")
                        .param("page", "0")
                        .param("size", "10")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true);

        DadosBeneficiarios responseBody = null;
        try {
            String json = result.getResponse().getContentAsString();
            responseBody = objectMapper.readValue(json, new TypeReference<DadosBeneficiarios>() {});

        } catch (Exception e) {
            e.printStackTrace();
        }

        // assert
        assertNotNull(responseBody);
        assertNotEquals(responseBody.beneficiarios.size(), 0);
        
        assertEquals(responseBody.beneficiarios().get(0).id(), beneficiario.getId());
        assertEquals(responseBody.beneficiarios().get(0).nome(), beneficiario.getNome());
        assertEquals(responseBody.beneficiarios().get(0).email(), beneficiario.getEmail());
        assertEquals(responseBody.beneficiarios().get(0).cpf(), beneficiario.getCpf());
        // todas as outras assertions que quiser fazer...
    }

Agora, sobre sua última resposta, você falou que queria que retornasse erro 500, então a abordagem para alcançar isso será diferente. Você precisa induzir o método a lançar um erro que é tratado genéricamente pelo ErrorHandler. Tecnicamente esse teste seria uma forma indireta de testar o ErrorHandler. Para isso criei esse teste:

    @Test
    @DisplayName("Deveria devolver código http 500, quando as informações estiverem inválidas")
    @WithMockUser
    void listBeneficiaryCenario02() throws Exception {
        // arrange
        when(repository.findAllByAtivoTrue(any(Pageable.class))).thenThrow(NullPointerException.class);
        
        // act
        var result = mvc.perform(get("/pacientes/teste")
                .param("page", "0")
                .param("size", "10")
                .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();
        
        // assert
        assertEquals(result.getStatus(), HttpStatus.INTERNAL_SERVER_ERROR.value());
    }

Forcei uma NullPointerException, assim a responsabilidade cai sobre o ErrorHandler da API, que trata esse erro e nos responde como esperamos. Tenicamente esse teste não testa o nosso controller, ele usa o Controller como meio para testar indiretamente a eficácia do ErrorHandler.

Oi! Então, na verdade, o último erro que eu te passei retornou 500, ao invés de ser 200, que é o cenário que estou tentando testar e ainda continua tendo este resultado, tentando testar o cenário de sucesso (http 200). Pelo que estou vendo, então eu tenho que passar o objeto Documentos, que está nulo, correto?

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /beneficiaries
       Parameters = {page=[0], size=[10]}
          Headers = [Accept:"application/json"]
             Body = null
    Session Attrs = {}
Handler:
             Type = com.plano.saude.cadastro.controller.BeneficiarioController
           Method = com.plano.saude.cadastro.controller.BeneficiarioController#listBeneficiary(Pageable)
Async:
    Async started = false
     Async result = null
Resolved Exception:
             Type = java.lang.NullPointerException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 500
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json", Content-Length:"158", X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0"]
     Content type = application/json
             Body = Error: Cannot invoke "java.util.List.stream()" because the return value of "com.plano.saude.cadastro.domain.beneficiario.Beneficiario.getDocumentos()" is null
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

java.lang.AssertionError: Status expected:<200> but was:<500>
Expected :200
Actual   :500
<Click to see difference>
    at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
    at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
    at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:637)
    at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)
    at com.plano.saude.cadastro.controller.BeneficiarioControllerTest.listBeneficiaryCenario2(BeneficiarioControllerTest.java:189)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    @Test
    @DisplayName("Deveria devolver código http 200, quando as informações estiverem válidas")
    @WithMockUser
    void listBeneficiaryCenario2() throws Exception {
        // arrange

        /**
         * Usada as anotações @Builder e @AllArgsConstructor do Lombok na classe Beneficiario,
         * assim ele gera um método estático que retorna um builder do objeto.
         */
        Beneficiario beneficiario = Beneficiario.builder()
                .id(1L)
                .nome("Beneficiario-san")
                .telefone("1145785230")
                .dataInclusao(LocalDate.parse("2018-03-18"))
                .dataNascimento(LocalDate.parse("1983-04-14"))
                .dataAtualizacao(LocalDate.parse("2023-11-23"))
                .build();

        Page<Beneficiario> beneficiarios = new PageImpl<>(List.of(beneficiario));
        when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(beneficiarios);

        // act
        var result = mvc.perform(get("/beneficiaries")
                    .param("page", "0")
                    .param("size", "10")
                    .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true);

        Page<DadosListagemBeneficiario> responseBody = null;
        try {
            String json = result.getResponse().getContentAsString();
            responseBody = objectMapper.readValue(json, new TypeReference<PageImpl<DadosListagemBeneficiario>>() {});
        } catch (Exception e) {
            e.printStackTrace();
        }

        // assert
        assertThat(responseBody).isNotNull();
        assertThat(responseBody.getContent()).hasSize(1);

        assertEquals(responseBody.getContent().get(0).id(), beneficiario.getId());
        assertEquals(responseBody.getContent().get(0).nome(), beneficiario.getNome());
        assertEquals(responseBody.getContent().get(0).telefone(), beneficiario.getTelefone());
        assertEquals(responseBody.getContent().get(0).dataInclusao(), beneficiario.getDataInclusao());
        assertEquals(responseBody.getContent().get(0).dataNascimento(), beneficiario.getDataNascimento());
        assertEquals(responseBody.getContent().get(0).dataAtualizacao(), beneficiario.getDataAtualizacao());
    }

Parece que lançou uma NullPointerException porque o objeto que é retornado pelo getDocumentos é uma lista não inicializada, fazendo com que ao chamar o método stream lance a NullPointerException... Ou esse objeto que é Beneficiario é null. Veja se esse atributo documentos é uma lista e inicialize ele com um = new ArrayList<>()

Tens esse projeto no GitHub? Consegues compartilhar com a gente?

Sim, retornou NullPointerException, porque o objectMapper retornou null. Vou te mandar o link no GitHub para ver melhor. https://github.com/amfreitas1981/test-plano-beneficiario/tree/dev_ale

Agora vai... Tinha dois problemas acontecendo:

  • Seu atributo List<Documento> no seu objeto Beneficiario é uma lista não inicializada, o que pode acarretar em NullPointerException quando alguma instrução tentar acessar um método do atributo documentos.

  • O seu objeto Beneficiario recebe um List<Documento>, mas esse atributo não foi passado quando você construiu ele (vi que atualmente no repositório foi passado sim, mas talvez quando você postou esse erro não tinha passado), e assim como falei no primeiro tópico, isso pode, e irá, acarretar em NullPointerException.

  • Com a falta do documentos é lançada uma NullPointerException quando você chama o método stream no construtor da classe DadosListagemBeneficiario, pois como a seu documentos é uma lista não inicializada, enquanto não for passada a variável de uma lista inicializada (mesmo que seja vazia) o valor dele será NULL.

public record DadosListagemBeneficiario(
        
        Long id,
        String nome,
        String telefone,
        LocalDate dataNascimento,
        LocalDate dataInclusao,
        LocalDate dataAtualizacao,
        List<DadosListagemDocumento> documentos,
        Endereco endereco) {
    
    public DadosListagemBeneficiario(Beneficiario beneficiario){
        this(
                beneficiario.getId(),
                beneficiario.getNome(),
                beneficiario.getTelefone(),
                beneficiario.getDataNascimento(),
                beneficiario.getDataInclusao(),
                beneficiario.getDataAtualizacao(),
                beneficiario.getDocumentos().stream().map(DadosListagemDocumento::new).toList(), // o erro é lançado aqui
                beneficiario.getEndereco()
        );
    }
}
  • Um caminho para não tomar NullPointerException nesse caso seria apenas você dar um = new ArrayList<>() no seu atributo documentos. No caso do meu teste, eu populei todas as propriedades para evitar erros.

O segundo problema parece que estava nos seus atributos LocalDate, o Jackson não tava sabendo lidar com eles e acabava retornando null. Então adicionei a configuração objectMapper.registerModule(new JavaTimeModule());

solução!

O teste na prática ficou assim:


    @JsonIgnoreProperties(ignoreUnknown = true)
    private record DadosBeneficiarios(@JsonProperty("content") List<DadosListagemBeneficiario> beneficiarios) {};

    @Test
    @DisplayName("Deveria devolver código http 200, quando as informações estiverem válidas")
    @WithMockUser
    void listBeneficiaryCenario2() throws Exception {
        // arrange
        List<Documento> documentos = List.of(Documento.builder()
                .id(1L)
                .tipoDocumento(TipoDocumento.CPF)
                .numero("40420854070")
                .dataExpedicao(LocalDate.now().minusMonths(3L))
                .descricao("Uma descricao")
                .dataInclusao(LocalDate.now().minusDays(10L))
                .dataAtualizacao(LocalDate.now().minusDays(5L))
                .ativo(true)
                .build());
        
        Beneficiario beneficiario = Beneficiario.builder()
                .id(1L)
                .nome("Beneficiario-san")
                .telefone("47940028922")
                .dataNascimento(LocalDate.now().minusYears(26L))
                .dataInclusao(LocalDate.now().minusDays(10L))
                .dataAtualizacao(LocalDate.now().minusDays(5L))
                .documentos(documentos)
                .ativo(true)
                .endereco(new Endereco())
                .build();
        documentos.get(0).setBeneficiario(beneficiario);
        
        Page<Beneficiario> beneficiarios = new PageImpl<>(List.of(beneficiario));
        when(beneficiarioRepository.findAllByAtivoTrue(any(Pageable.class))).thenReturn(beneficiarios);

        // act
        var result = mvc.perform(
                get("/beneficiaries")
                        .param("page", "0")
                        .param("size", "10")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andReturn();
        
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true);
        objectMapper.registerModule(new JavaTimeModule());
        
        DadosBeneficiarios responseBody = null;
        try {
            String json = result.getResponse().getContentAsString();
            responseBody = objectMapper.readValue(json, new TypeReference<DadosBeneficiarios>() {});

        } catch (Exception e) {
            e.printStackTrace();
        }
        DadosListagemBeneficiario objectResult = responseBody.beneficiarios().get(0);

        // assert
        assertNotNull(responseBody);
        assertFalse(responseBody.beneficiarios().isEmpty());
        assertEquals(1, responseBody.beneficiarios().size());

        assertEquals(objectResult.id(), beneficiario.getId());
        assertEquals(objectResult.nome(), beneficiario.getNome());
        assertEquals(objectResult.telefone(), beneficiario.getTelefone());
        assertEquals(objectResult.dataInclusao(), beneficiario.getDataInclusao());
        assertEquals(objectResult.dataNascimento(), beneficiario.getDataNascimento());
        assertEquals(objectResult.dataAtualizacao(), beneficiario.getDataAtualizacao());
        
//        assertEquals(objectResult.documentos(), beneficiario.getDocumentos()); // precisa comparar os atributos, pois os objetos são diferentes
        
        DadosListagemDocumento documentoResult = objectResult.documentos().get(0);
        Documento documentoBeneficiario = beneficiario.getDocumentos().get(0);
        assertEquals(documentoResult.id(), documentoBeneficiario.getId());
        assertEquals(documentoResult.tipoDocumento(), documentoBeneficiario.getTipoDocumento());
        assertEquals(documentoResult.numero(), documentoBeneficiario.getNumero());
        assertEquals(documentoResult.dataExpedicao(), documentoBeneficiario.getDataExpedicao());
        assertEquals(documentoResult.descricao(), documentoBeneficiario.getDescricao());
        assertEquals(documentoResult.dataInclusao(), documentoBeneficiario.getDataInclusao());
        assertEquals(documentoResult.dataAtualizacao(), documentoBeneficiario.getDataAtualizacao());
        
//        assertEquals(objectResult.endereco(), beneficiario.getEndereco()); // passei o endereço com atributos null, não tem necessidade de valida-los
    }

Oi Mateus! Vi a sua solução, ficou bem legal, funcionou, certinho para o teste de sucesso e até atualizei o código. Obrigado pela ajuda! Tenho mais uma dúvida, mas para testar o cenário de erro 404, como que eu faço para simular o Pageable?

Se precisarem, segue a solução que eu consegui resolver para testar o cenário para o erro 404:

    @Test
    @DisplayName("Deveria devolver código http 404, quando as informações estão inválidas")
    @WithMockUser
    void listBeneficiaryCenario1() throws Exception {
        // Simular que o pageable não existe
        given(beneficiarioRepository.findAllByAtivoTrue(any())).willThrow(EntityNotFoundException.class);

        var response = mvc.perform(get("/beneficiaries")
                        .param("page", "0")
                        .param("size", "10")
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
    }
    
    ```