> Java > java지도 시간 > 본문

Spring과의 통합 테스트

PHPz
풀어 주다: 2024-08-23 22:32:36
원래의
258명이 탐색했습니다.

여기서 아이디어는 무엇입니까?

" 모든 것이 이미 준비된 후에” 회사의 품질 관문에 도달하기만 하면 됩니다.

자, 나누어서…

통합 테스트

모든 것을 통과하고 모든 지점을 통과하는 횡단적인 방식으로 애플리케이션을 테스트할 수 있으므로 HTTP 호출은 모든 코드 구성 요소를 통과하고 서로 어떻게 작동하는지 테스트합니다. 이렇게 하면 전체 애플리케이션 흐름이 작동한다는 것을 보장할 수 있지만 일반적으로 더 많은 비용이 드는 테스트 유형입니다. 예, 훨씬 더 많은 리소스를 소비하므로 비용이 많이 듭니다. 이를 측정하는 방법은 이러한 테스트를 실행하는 데 걸리는 시간을 관찰하는 것뿐입니다. .

TDD

여기서 제 견해에 대해 자유롭게 질문해 주세요

여기서의 의도는 통합 테스트를 사용하여 애플리케이션 구축을 시작하는 것이지만 반드시 TDD일 필요는 없습니다. TDD의 아이디어는 이전에 말했듯이 단위 테스트를 구축하는 것입니다. 단위 테스트는 더 세분화되어 있습니다. 통합된 구성 요소에 의존하지 않고 각 코드 단위를 테스트하므로 테스트된 구성 요소 외부 동작의 영향을 받지 않습니다.

이를 염두에 두고 애플리케이션을 구축하는 방법이 될 수 있지만 통합 테스트를 실행하는 데 더 많은 시간이 걸리므로 TDD에서 제안하는 빠른 피드백 주기가 지연된다는 점을 명심하는 것이 중요합니다. 하지만 그래도 그렇습니다. 개발에는 테스트가 포함됩니다.

프로젝트

여기서 아이디어는 간단한 프로젝트, Rest API 및 4가지 기본 작업(예전의 CRUD, CRUD가 무엇인지 아시나요? 그렇지 않으니 여기를 읽어도 괜찮습니다)을 제공하고 해당 프로젝트에서 각 엔드포인트를 구축하는 것입니다. 시험을 치르세요.

그러면 서비스 주문 API가 제공됩니다.

  • POST /주문: 다음 요청을 받게 됩니다:


    {
      "description": "Some description"
    }
    
    로그인 후 복사
    데이터베이스에 서비스 주문을 생성하고 생성된 주문을 다음과 같이 반환합니다.


    {
      "id": "xxxxxxxxxxxxx",
      "description": "Some description",
      "created_at": "2024-02-21T23:58:01:000z",
      "updated_at": "2024-02-21T23:58:01:000z",
      "status": "OPENED"
    }
    
    로그인 후 복사
  • GET /orders: 모든 은행 주문을 나열합니다(물론 잘못된 모든 항목을 나열하지만 부분적으로 나열하겠습니다).

다음 형식으로 데이터베이스에서 서비스 주문 목록을 반환합니다.


```json
[
  {
    "id": "xxxxxxxxxxxxx",
    "description": "Some description",
    "created_at": "2024-02-21T23:58:01:000z",
    "updated_at": "2024-02-21T23:58:01:000z",
    "status": "OPENED"
  }
]
```
로그인 후 복사
  • PUT /orders/:id: 아래와 같은 요청을 받게 되며 요청 경로에 기존 주문의 ID가 알려집니다.


    {
      "description": "Some description",
      "status": "CLOSED"
    }
    
    로그인 후 복사
데이터베이스에 서비스 주문을 생성하고 생성된 주문을 다음과 같이 반환합니다.


```json
{
  "id": "xxxxxxxxxxxxx",
  "description": "Some description",
  "created_at": "2024-02-21T23:58:01:000z",
  "updated_at": "2024-02-23T13:35:09:135z",
  "status": "OPENED"
}
```
로그인 후 복사
존재하지 않는 주문의 ID를 입력하면 StatusCode 404와 함께 오류가 반환되어야 합니다.

  • GET /orders/:id

    요청 경로에 통보된 기존 주문의 ID를 수신하고, 데이터베이스를 쿼리하고 다음과 같은 기록을 반환합니다.

    {
      "id": "xxxxxxxxxxxxx",
      "description": "Some description",
      "created_at": "2024-02-21T23:58:01:000z",
      "updated_at": "2024-02-23T13:35:09:135z",
      "status": "OPENED"
    }
    
    로그인 후 복사
존재하지 않는 주문의 ID를 입력하면 상태 코드 404와 함께 오류가 반환되어야 합니다.

    DELETE /orders/:id: 요청 경로에 안내된 기존 주문의 ID를 받게 됩니다. 여기에는 페이로드가 없습니다.
어떤 기술?

여기에서는 불멸의 Java를 사용하겠습니다

Testes integrados com Spring

그리고 성공적인 파트너십 중 일부는 다음과 같습니다.

  • 포스트그레SQL
이것이 우리 애플리케이션의 핵심이 될 것이지만 우리의 목적에 맞게 몇 가지 libs를 추가할 것입니다. 먼저 H2 데이터베이스에 대해 스스로에게 질문해 보아야 합니다. "- 데이터베이스가 두 개인가?" H2는 테스트용으로만 사용됩니다. 이는 메모리에서 작동하는 데이터베이스이며 여기에서 우리의 목적에 이상적이며 데이터베이스가 올바른 목적으로 사용되는 한 많은 실제 애플리케이션 시나리오에서 데이터를 저장합니다( 여기에도 의견 차이가 있습니다. 단지 제 충격적인 의견일 뿐입니다.

프로젝트를 생성하기 위해 Spring 초기화 웹사이트를 사용할 것입니다. 다음과 같은 것을 생성합니다:

Testes integrados com Spring

여기에 추가된 라이브러리와 선택한 버전에 주의하세요.

를 참조하세요.

    봄: 3.3.1
  • 자바: 21
Maven을 사용하여 종속성을 관리하고 프로젝트 빌드를 수행하겠습니다.

Conforme podemos ver na imagem acima ao clicar no botão “Generate” um arquivo será gerado .zip com um projeto base, descompactaremos em lugar de preferência e iremos abri-lo na nossa IDE preferida, aqui usaremos Intellij CE. Isso pode ser importante ressaltar por que alguns exemplos podem ser dados estritamente usando a ferramenta, por simples questão de agilidade, mas tendo o conhecimento necessário, qualquer ferramenta serve.

A estrutura resultante dentro da pasta deve ser algo assim:

Testes integrados com Spring

Veja que por padrão já teremos um package test, será nele que iremos criar nosso teste. Vamos criar um package e chamá-lo de integration:

Testes integrados com Spring

Vamos começar por um teste simples que nos permita estruturar nossa classe de teste, vamos testar nosso primeiro endpoint que irá nos retornar uma lista vazia das nossas ordens. Primeiro momento vamos criar um teste somente para efetuar toda a configuração necessária para nosso teste.

Sabendo que temos um teste de integração e nosso contexto terá um banco de dados, vamos começar fornecendo ao Spring as configurações que ele precisa para alcançar estes recursos, no nosso caso o banco de dados H2(lembra dele, mas fixando, somente para os nosso testes). Assim criaremos um arquivo application.yaml em src/test/resources com o seguinte conteúdo:

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb
    username: sa
    password: password
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
    ddl-auto: create-drop
    show-sql: true
  h2:
    console:
    enabled: true
로그인 후 복사

Dentro da pasta acima que acabamos de criar vamos criar uma classes que chamaremos OrderControllerIntegrationTest, ela tera a seguinte estrutura:

package com.seuprojeto.integrationtest.integration;

import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@AutoConfigureMockMvc
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OrderControllerIntegrationTest {

    private static final String ORDER_URL = "/orders";

    @Autowired
    MockMvc mockMvc;
}

로그인 후 복사

Vamos falar sobre cada uma das linhas:

  1. @TestInstance(TestInstance.Lifecycle.PER_CLASS): Garantir que somente uma instância da classe de teste será criada. Mais especificamente, o Junit5 por padrão cria uma nova instância da classe para cada teste que implementamos, isso especialmente para garantir total isolamento entra cada caso de teste, no entanto, isso é um tanto quanto custoso se você tem cenário que exigem muita configuração para serem executados. Isso nos remete ao início deste artigo, onde falamos que testes de integração podem ser custosos, dessa forma usaremos essa anotação para que uma instância seja criada para cada classe, assim permitindo compartilhar o estado de alguns objetos (não serão criados e destruídos a cada teste), neste caso o MockMvc, que é o que usaremos para fazer nossas chamadas HTTP. Ops, meio que começamos a falar da linha seguinte. Isso irá nos permitir economizar um pouco de recursos na execução dos nossos testes, tornando um pouco mais rápidos também.
  2. @AutoConfigureMockMvc: Fará a configuração automática do objeto MockMvc, que usaremos para fazer nossas chamadas HTTP.
  3. @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD): Informar que o contexto do Spring foi alterado a cada teste, assim garante que o contexto do Spring seja reiniciado a cada caso de teste. Isso impede que alterações no contexto do Spring feitas em um teste não impacte em outro teste. Neste ponto aqui é importante compreender o que o contexto do Spring e como ele funciona, o que não iremos aprofundar.
  4. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT): Indica que iremos usar testes de integração do spring, garantindo assim que o Spring faça a devida manipulação do contexto.

Passamos mais detalhadamente por essas linhas por que no contexto de configuração de testes essa classes são bastante importantes e requerem um pouco de detalhamento sobre elas, as demais classes são coisas mais “simples” e vamos falar em blocos.

private static final String ORDER_URL = "/orders";

@Autowired
MockMvc mockMvc;
로그인 후 복사

Declaramos uma propriedade estática para o uri, porque iremos repeti-la algumas vezes e por boa prática vamos guardá-la em uma variável e na sequência injetamos o objeto MockMvc que usaremos para fazer nossas chamadas HTTP e fazer nossos testes.

Primeiro teste

Este primeiro só irá garantir que todo o aparato de configurações estejam funcionando, então neste momento não iremos no aprofundar muito, iremos fazer isso a medida que nossos cenários de teste se tornem mais maduros.

@Test
void shouldReturn200WhenGetAllOrders() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get(ORDER_URL))
            .andExpect(status().isOk())
            .andReturn();
}
로그인 후 복사

Considerando que não temos nenhuma entidade qualquer criada em nossa aplicação, então este teste espera que ao fazer uma chamada GET para a URI /orders devemos ter como retorno um StatusCode 200.

Uma adendo aqui, ao construir o caso de teste na nossa classe, para executar basta clicar no botão exibido pela IDE (Eu disse que usaríamos dos recursos da IDE):

Testes integrados com Spring

Ao executar este teste receberemos um caloroso 404, obviamente, já que não implementamos esta rota em nossa aplicação ainda.

Testes integrados com Spring

Então vamos fazer este teste passar. Para isso iremos criar a controller que irá responder a nossa chamada, ignore aqui a maneira com que classes e packages serão criados, não iremos discutir sobre isso neste artigo, além do mais considerando que o spring é responsável por gerenciar a injeção de dependência, a estrutura de como as packages são criadas não é necessariamente importante. Vamos lá, iremos criar a seguinte package com/seuprojeto/integration/app/controller:

Testes integrados com Spring

Dentro dela iremos criar nossa controller, como abaixo:

package com.seuprojeto.integrationtest.app.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/orders")
public class OrderController {

    @GetMapping
    public List<?> getAllOrders() {
        return List.of();
    }
}
로그인 후 복사

Não é o que pretendemos, mas essa simples implementação fará nossos testes passarem com sucesso, isso irá validar que nosso testes estão, por hora, configurados com sucesso.

Segundo teste

Bem, agora vamos um pouco mais fundo, começar a fazer testes que, de fato, irão atravessar camadas da nossa aplicação. Vamos implementar o caso de para criação de uma ordem e persistir no banco de dados.

Este será nosso segundo caso de teste, mais uma vez vamos por partes aqui, não é um teste conforme esperado e iremos melhora-lo:

@Test
void shouldReturn201WhenCreateOrder() throws Exception {
    var response = mockMvc.perform(MockMvcRequestBuilders.post(ORDER_URL).content("{\"description\": \"some description\"}").contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isCreated())
            .andReturn();

    System.out.println(response.getResponse().getContentAsString());

    Assertions.assertTrue(response.getResponse().getContentAsString().contains("description"));
    Assertions.assertTrue(response.getResponse().getContentAsString().contains("some description"));
    Assertions.assertTrue(response.getResponse().getContentAsString().contains("id"));
    Assertions.assertTrue(response.getResponse().getContentAsString().contains("createdAt"));
    Assertions.assertTrue(response.getResponse().getContentAsString().contains("updatedAt"));
    Assertions.assertTrue(response.getResponse().getContentAsString().contains("status"));
    Assertions.assertTrue(response.getResponse().getContentAsString().contains("OPENED"));
}
로그인 후 복사

Se executarmos agora teremos um StatusCode 405, ou seja, o rota /orders foi encontrada, mas o método POST não esta implementado, vamos cuidar disso.

Testes integrados com Spring

Primeiro vamos criar nossa classe de domínio Order, ela será nossa e também nosso entidade para banco de dados. Criaremos em um novo package com/seuprojeto/integration/domain e dentro dele as classes abaixo:

package com.seuprojeto.integrationtest.domain;

import jakarta.persistence.*;

import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Table(name = "orders", schema = "public")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    private String description;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private Status status;

    public Order() {
    }

    public Order(UUID id, String description, LocalDateTime createdAt, LocalDateTime updatedAt, Status status) {
        this.id = id;
        this.description = description;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.status = status;
    }

    public static Order create(String description) {
        return new Order(UUID.randomUUID(), description, LocalDateTime.now(), LocalDateTime.now(), Status.OPENED);
    }

    public UUID getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }

    public Status getStatus() {
        return status;
    }
}

로그인 후 복사
package com.seuprojeto.integrationtest.domain;

public enum Status {
    OPENED, CLOSED, CANCELED;
}
로그인 후 복사

Pensando no isolamento das nossas classes de domínio das extremidades da nossa aplicação não iremos expor-las nas bordas do sistema, então criaremos dois records DTO para fazer a comunicação com partes eternas de nossos sistemas, no caso a interface HTTP Rest, assim criaremos duas classes em com/seuprojeto/integration/app/controller/dto .

Este primeiro é a estrutura que irá receber o JSON para criação de uma ordem, conforme contrato estabelecido.

public record CreateOrderDto(String description) {
}
로그인 후 복사

Este segundo tem o contrato conforme esperado para saída do método de criação.

package com.seuprojeto.integrationtest.app.controller.dto;

import com.seuprojeto.integrationtest.domain.Order;

public record OrderCreatedDto(String id, String description, String status, String createdAt, String updatedAt) {
    public static OrderCreatedDto from(Order order) {
        return new OrderCreatedDto(
                order.getId().toString(),
                order.getDescription(),
                order.getStatus().name(),
                order.getCreatedAt().toString(),
                order.getUpdatedAt().toString());
    }
}
로그인 후 복사

E agora iremos criar a classe de acesso a banco de dados, normalmente aqui eu adicionaria mais uma camada, de usecase, para que cada fluxo da aplicação ficasse devidamente separado, mas não irei me alongar ainda mais.

Então vamos ajustar nossa controller para permitir atender nossos requisitos de teste, assim nossa classe ficará assim:

package com.seuprojeto.integrationtest.app.controller;

import com.seuprojeto.integrationtest.app.controller.dto.CreateOrderDto;
import com.seuprojeto.integrationtest.app.controller.dto.OrderCreatedDto;
import com.seuprojeto.integrationtest.domain.Order;
import com.seuprojeto.integrationtest.infra.OrderRepository;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderRepository repository;

    public OrderController(OrderRepository repository) {
        this.repository = repository;
    }

    @GetMapping
    public List<?> getAllOrders() {
        return List.of();
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public OrderCreatedDto createOrder(@RequestBody CreateOrderDto createOrderDto) {
        final Order order = Order.create(createOrderDto.description());
        final Order orderSaved = this.repository.save(order);
        return OrderCreatedDto.from(orderSaved);
    }
}

로그인 후 복사

E voilá, nosso teste irá passar com sucesso.

Mas voltemos a composição do nosso teste, ele não muito apresentável, e além de tudo passível de algumas falhas. Primeiro que não esta legal é o fato de que estamos usando muitas validações com string, isso fica ruim para mater além de um tanto verboso. Vamos fazer algumas melhorias então, veja que nossa classe controller recebe e retorna classes, podemos utilizar as mesmas para compor nosso teste. Então vamos fazer alterações na nossa classe de teste, primeiro adicionando um novo objeto:

package com.seuprojeto.integrationtest.integration;

import com.fasterxml.jackson.databind.ObjectMapper;
...
class OrderControllerIntegrationTest {

    private static ObjectMapper objectMapper = new ObjectMapper();
...
로그인 후 복사

Obs.: Algumas partes da classe foram omitidas, para dar enfase ao que foi adicionado.

ObjectMapper usaremos para fazer a serialização das nossas classes(records) de entrada e saída, melhorando a compreensão de nossos testes. Vamos refatorar nosso caso de teste conforme abaixo:

...
import java.util.regex.Pattern;
...
@Test
void shouldReturn201WhenCreateOrder() throws Exception {
    final String description = "some description";
    final CreateOrderDto createOrderDto = new CreateOrderDto(description);
    final String payload = objectMapper.writeValueAsString(createOrderDto);

    final MvcResult response = this.mockMvc.perform(MockMvcRequestBuilders.post(ORDER_URL).content(payload).contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isCreated())
            .andReturn();

    OrderCreatedDto orderCreatedDto = objectMapper.readValue(response.getResponse().getContentAsString(), OrderCreatedDto.class);

    Pattern pattern = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");

    Assertions.assertNotNull(orderCreatedDto.id());
    Assertions.assertTrue(pattern.matcher(orderCreatedDto.id()).matches());
    Assertions.assertEquals(description, orderCreatedDto.description());
    Assertions.assertNotNull(orderCreatedDto.createdAt());
    Assertions.assertNotNull(orderCreatedDto.updatedAt());
    Assertions.assertEquals("OPENED", orderCreatedDto.status());
}
로그인 후 복사

Agora nosso teste estão diretamente vinculados a nossas classes, assim quando houverem alterações nessas, será mais fácil identificar onde os teste foram quebrados, tão quanto fazer os ajustes necessários, tão como ficou mais legível o que está sendo testado.

Terceiro teste

Vamos agora testar nosso endpoint de alteração, neste endpoint vimos que necessário informar o campo status, então vamos criar um record específico para este endpoint, conforme abaixo:

package com.seuprojeto.integrationtest.integration.app.controller.dto;

public record UpdateOrderDto(String description, String status) {
}
로그인 후 복사

Vamos também alterar uma classe já existe, a classe order, adicionando a ela um método que irá atualizar os dados da classe. Adicionaremos o método abaixo:

...
public void update(String description, String status) {
    this.description = description;
    this.status = Status.valueOf(status.toUpperCase());
    this.updatedAt = LocalDateTime.now();
}
...
로그인 후 복사

E então criaremos o método na controller

...
@PutMapping("/{id}")
public OrderCreatedDto updateOrder(@PathVariable String id, @RequestBody UpdateOrderDto updateOrderDto) {
    final UUID uuid = UUID.fromString(id);
    final Order order = this.repository.findById(uuid).orElseThrow();
    order.update(updateOrderDto.description(), updateOrderDto.status());
    final Order orderSaved = this.repository.save(order);
    return OrderCreatedDto.from(orderSaved);
}
로그인 후 복사

Neste teste teremos uma peculiaridade, pois para alterar uma ordem precisamos primeiro, precisamos primeiro saber de uma ordem já existente, pois a uri para atualizar um ordem exige como parâmetro um id, então faremos a chamada do endpoint de criação de ordem e usaremos o retorno deste para fazer a chamada no nosso endpoint PUT.

@Test
void shouldReturn200WhenPutAnExistentOrder() throws Exception {
    final String description = "some description";
    final CreateOrderDto createOrderDto = new CreateOrderDto(description);
    final String payload = objectMapper.writeValueAsString(createOrderDto);

    final MvcResult response = this.mockMvc.perform(MockMvcRequestBuilders.post(ORDER_URL).content(payload).contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isCreated())
            .andReturn();

    final OrderCreatedDto orderCreatedDto = objectMapper.readValue(response.getResponse().getContentAsString(), OrderCreatedDto.class);

    final String newDescription = "new description";
    final String newStatus = "CLOSED";
    final UpdateOrderDto updateOrderDto = new UpdateOrderDto(newDescription, newStatus);
    final String updatePayload = objectMapper.writeValueAsString(updateOrderDto);

    final MvcResult updateResponse = this.mockMvc.perform(MockMvcRequestBuilders.put(ORDER_URL + "/" + orderCreatedDto.id()).content(updatePayload).contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andReturn();

    final OrderCreatedDto updatedOrderCreatedDto = objectMapper.readValue(updateResponse.getResponse().getContentAsString(), OrderCreatedDto.class);
    Assertions.assertEquals(newDescription, updatedOrderCreatedDto.description());
    Assertions.assertEquals(newStatus, updatedOrderCreatedDto.status());
}
로그인 후 복사

Voltando para o primeiro caso de teste

Embora este teste com certeza esta passando sem erro, voltemos a ele. Este teste faz uma simples validação se é retornado um StatusCode 200, vamos alterar ele já que agora temos de fato nossa aplicação persistindo em banco de dados, então vamos alterar primeiramente o método que retorna a lista de ordens persistida no banco:

@GetMapping
public List<OrderCreatedDto> getAllOrders() {
    return this.repository.findAll().stream()
            .map(OrderCreatedDto::from)
            .toList();
}
로그인 후 복사

E nossa classe de teste iremos alterar assim:

@Test
void shouldReturn200WhenGetAllOrders() throws Exception {
    final MvcResult response = mockMvc.perform(MockMvcRequestBuilders.get(ORDER_URL))
            .andExpect(status().isOk())
            .andReturn();

    Assertions.assertTrue(response.getResponse().getContentAsString().contains("[]"));
}
로그인 후 복사

Executando todos os testes veremos que eles passarão com sucesso.

E ai deve vir a sua cabeça, ao executar todos os testes veremos que método que criação de ordens são escutados antes, no entanto, vimos que testamos um retorno de lista vazia, por que? É importante compreender isso, primeiro lembra-se da annotation DirtiesContext? Bem, ela garante que a cada teste nosso contexto seja reiniciado, assim os dados criados em cada um dos teste são “descartados”, a cada teste o banco que é um banco H2 em memória, é recriado, isso nos garantes isolamentos dos testes, impedindo que um caso de teste influencie outro, causando o que chamamos de flaky test. Essa hipótese pode ser testa simplesmente comentando a linha:

//@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
로그인 후 복사

E executando os testes novamente. Veja embora isso causa esta falta de isolamento dos teste reiniciar o contexto do Spring consome mais recursos. É uma opção não habilitar esta opção, mas isso nos exigirá algumas configurações adicionais em nossos testes.

Testes integrados com Spring

Tempo de execução com annotation que reinicia o contexto do Spring

Testes integrados com Spring

Tempo de execução sem o annotation que reinicia o contexto Spring

Podemos ver que temos um ganho de tempo na execução considerável, em uma suíte de testes extensa, este ganho pode ser considerável, vamos então fazer as configurações necessário e vamos seguir assim para ser mais rápida a nossa execução. Esta é uma decisão que tem alguns tradeoffs, então antes de tomar essa decisão avalie os riscos. Vamos as configurações na nossa classe de testes:

package com.seuprojeto.integrationtest.integration;

...
import com.seuprojeto.integrationtest.infra.OrderRepository;
...
import org.junit.jupiter.api.BeforeEach;
...
import org.springframework.beans.factory.annotation.Autowired;
...

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@AutoConfigureMockMvc
@EnableConfigurationProperties
//@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderControllerIntegrationTest {

    @Autowired
    OrderRepository orderRepository;

    ...

    @Autowired
    MockMvc mockMvc;

    @BeforeEach
    void setUp() {
         this.orderRepository.deleteAll();
    }
    ...
}
로그인 후 복사

O que mudamos aqui é o seguinte, injetamos a nossa classe OrderRepository e adicionamos o método setup. O método setup possui a annotation @BeforeEach, isso irá fazer com que o conteúdo dele seja executado sempre antes de executar um novo caso de teste. Dentro dele chamamos um método que remove todos os registros da tabela, assim evitando que nosso casos de teste tenham implicações uns sobre os outros (ao menos neste caso).

Quarto teste

Vamos testar o endpoint capaz de retornar uma única ordem com base em seu id. Este será um teste mais fácil visto que não teremos nada de novo para criar aqui para que o teste funcione. Vejamos como fica o método que retorna a ordem de serviço na controller:

@GetMapping("/{id}")
public OrderCreatedDto getOrder(@PathVariable String id) {
    final UUID uuid = UUID.fromString(id);
    final Order order = this.repository.findById(uuid).orElseThrow();
    return OrderCreatedDto.from(order);
}
로그인 후 복사

E o teste será implementado assim:

@Test
void shouldReturn200WhenGetAnExistentOrder() throws Exception {
    final String description = "some description";
    final CreateOrderDto createOrderDto = new CreateOrderDto(description);
    final String payload = objectMapper.writeValueAsString(createOrderDto);

    final MvcResult response = this.mockMvc.perform(MockMvcRequestBuilders.post(ORDER_URL).content(payload).contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isCreated())
            .andReturn();

    final OrderCreatedDto orderCreatedDto = objectMapper.readValue(response.getResponse().getContentAsString(), OrderCreatedDto.class);

    final MvcResult getResponse = this.mockMvc.perform(MockMvcRequestBuilders.get(ORDER_URL + "/" + orderCreatedDto.id()))
            .andExpect(status().isOk())
            .andReturn();

    final OrderCreatedDto getOrderCreatedDto = objectMapper.readValue(getResponse.getResponse().getContentAsString(), OrderCreatedDto.class);
    Assertions.assertEquals(orderCreatedDto.id(), getOrderCreatedDto.id());
    Assertions.assertEquals(orderCreatedDto.description(), getOrderCreatedDto.description());
    Assertions.assertEquals(orderCreatedDto.status(), getOrderCreatedDto.status());
}
로그인 후 복사

Note que mais uma vez usamos a estratégia de adicionar um item através do método de criação e depois consulta-lo. É possível fazer essa inserção de dados diretamente no banco, já que o repository foi injetado em nossa classe de teste, particularmente acredito que isso é manipular diretamente os dados e fugiria o objetivo deste teste que é fazer testar somente através da interface que a aplicação irá exposta.

Quinto teste

Este é o teste de irá remover um registro do banco de dados, vamos ao método da controller:

@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteOrder(@PathVariable String id) {
    final UUID uuid = UUID.fromString(id);
    this.repository.deleteById(uuid);
}
로그인 후 복사

Nosso caso de teste:

@Test
void shouldReturn204WhenDeleteAnExistentOrder() throws Exception {
    final String description = "some description";
    final CreateOrderDto createOrderDto = new CreateOrderDto(description);
    final String payload = objectMapper.writeValueAsString(createOrderDto);

    final MvcResult response = this.mockMvc.perform(MockMvcRequestBuilders.post(ORDER_URL).content(payload).contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isCreated())
            .andReturn();

    final OrderCreatedDto orderCreatedDto = objectMapper.readValue(response.getResponse().getContentAsString(), OrderCreatedDto.class);

    this.mockMvc.perform(MockMvcRequestBuilders.delete(ORDER_URL + "/" + orderCreatedDto.id()))
            .andExpect(status().isNoContent());

}
로그인 후 복사

Mais uma vez chamamos o método que cria uma ordem e somente depois executamos o método que irá excluir/deletar o registro.

Veremos que este caso de teste passa sem problema, aqui basicamente fechamos todos o métodos do nosso CRUD. Mas vamos pensar no seguinte cenário, para este último caso, embora ele tenho funcionado corretamente podemos fazer uma validação adicional, pense que se a ordem foi removida, então significa que que se eu fizer aquele GET pelo id eu deveria ter um retorno 404. A implementação ficaria algo assim, este código adicionado ao fim do mesmo caso de teste:

@Test
void shouldReturn204WhenDeleteAnExistentOrder() throws Exception {
    ...

    this.mockMvc.perform(MockMvcRequestBuilders.get(ORDER_URL + "/" + orderCreatedDto.id()))
            .andExpect(status().isNotFound())
            .andReturn();
}
로그인 후 복사

Porém ao executar o teste novamente com esta nova parte implementação teste deixará de passar, pois não fizemos nenhuma implementação que nos retorne o StatusCode 404. Na verdade é retornada uma exceção. Bem é isso que implementamos no método de consulta:

Testes integrados com Spring

Se lembramos na nossa proposta de implementação este mesmo método 404 é pedido também em outros métodos no próprio DELETE e no PUT também. Então vamos aplicar uma solução que possa ser aplicada em todas as situações.

Para isso iremos gerar uma exception personalizada, que será usada sempre que eu tiver essa situação de ordem não encontrada. Na package com/seuprojeto/integration/domain iremos criar a seguinte classe:

package com.seuprojeto.integrationtest.domain;

public class OrderNotFoundException extends RuntimeException {
    public OrderNotFoundException(String id) {
        super("Order not found with id: " + id);
    }
}
로그인 후 복사

E agora vamos refatorar nosso método GET para gerar esse nossa exception:

@GetMapping("/{id}")
public OrderCreatedDto getOrder(@PathVariable String id) {
    final UUID uuid = UUID.fromString(id);
    final Order order = this.repository.findById(uuid).orElseThrow(() -> new OrderNotFoundException(id));
    return OrderCreatedDto.from(order);
}
로그인 후 복사

Legal, mas isso não irá resolver todos os nosso problemas, só mudamos o sabor do nosso erro. Agora iremos fazer o nossa aplicação interpretar que toda vez que essa exceção for gerada, nós iremos captura-la e gerar uma saída personalizada com o formato que desejamos. Para isso iremos criar uma nova classe para gerar essa configuração. Criaremos uma nova classe em com/seuprojeto/integration/app:

package com.seuprojeto.integrationtest.app.controller;

import com.seuprojeto.integrationtest.domain.OrderNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map;

@RestControllerAdvice
public class HandlerException {

    @ExceptionHandler(OrderNotFoundException.class)
    @ResponseStatus(code = HttpStatus.NOT_FOUND)
    public Map<String, String> handleOrderNotFoundException(OrderNotFoundException e) {
        return Map.of("message", e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, String> handleException(Exception e) {
        System.out.println(e.getMessage());
        return Map.of("message", "Internal server error");
    }
}
로그인 후 복사

Esta classe irá capturar nossas exceções, vamos começar falando do método:

public Map handleException(Exception e)

Este método irá fazer com o nosso framework capture quando uma exceção é gerada. Especificamente este método irá capturar qualquer exceção que ocorra e olhando mais atentamente vemos que irá retornar uma mensagem padrão com o seguinte formato:

{
  "message": "Internal server error"
}
로그인 후 복사

Aqui teremos um StatusCode 500. É uma boa prática este método pois isso evita que toda a stack da exception seja retornada ao usuário final que fez seja lá chamada para nosso aplicação, evitando expor detalhes técnicos da aplicação. Veja que antes do retorno “logamos”(Não necessariamente o estado da arte de logs de aplicação, certo?) a mensagem de erro original para facilitar possíveis investigações posteriores.

Porém este método é bastante genérico e não trata diretamente da nosso necessidade que é tratar nossa exceção personalizada OrderNotFoundException, por isso criamos um método específico para capturar e tratar nossa exception:

public Map<String, String> handleOrderNotFoundException(OrderNotFoundException e)
로그인 후 복사

Note que este método em sim tem uma annotation que define o StatusCode 404 (not found). Uma vez implementada nossa classe podemos chamar novamente nosso caso de teste que agora irá funcionar com total sucesso.

Sexto teste

Tendo em vista a implementação anterior sobre o retorno do 404 quando uma ordem não é encontrada, vamos implementar uma caso de teste para isso chamando o nosso GET com o id de uma ordem não existente.

@Test
void shouldReturn404WhenGetAnNonExistentOrder() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.get(ORDER_URL + "/123"))
            .andExpect(status().isNotFound())
            .andReturn();
} 
로그인 후 복사

Se simplesmente implementarmos o caso de teste acima e executa-lo, teremos um StatusCode 500, vamos refatorar o método em busca do nosso código 404. Mas por que este erro, se método na prática já foi refatorado e vimos anteriormente que ele funcionou corretamente, retornando 404?

Bem nosso método espera que seja informado um valor UUID válido para o id, como o valor informado é foi 123, ele não pode ser convertido em um UUID válido, gerando um exception do tipo java.lang.IllegalArgumentException. Poderíamos adicionar um tratativa para este tipo de exception no nosso handler de erro, mas está é uma classe que pode ser gerada em vários pontos da aplicação por outros motivos, trata-la de forma a retornar um 404, pode gerar uma informação de retorno imprecisa para um cliente da aplicação em alguns momentos, então vamos abordar de outra forma.

Veja a decisão tomada aqui visa não expor ao cliente muitos detalhes sobre a nossa aplicação e tenta manter de forma coerente a abordagem da nossa aplicação, outras decisões técnicas aqui pode ser adotadas, isso vai variar do seu negócio. Em nossa solução iremos criar um método na nosso controller:

package com.seuprojeto.integrationtest.app.controller;

...
import java.util.UUID;

@RestController
@RequestMapping("/orders")
public class OrderController {
   ...

    @GetMapping("/{id}")
    public OrderCreatedDto getOrder(@PathVariable String id) {
        final UUID uuid = getUuid(id);
        final Order order = this.repository.findById(uuid).orElseThrow(() -> new OrderNotFoundException(id));
        return OrderCreatedDto.from(order);
    }
  ...
    private static UUID getUuid(String id) {
        try {
            return UUID.fromString(id);
        } catch (IllegalArgumentException e) {
            throw new OrderNotFoundException(id);
        }
    }
}

로그인 후 복사

Veja que teremos o método getUuid e este método irá encapsular a conversão da nosso String recebida em um UUID válido, nosso contexto, desta classe especificamente, podemos considerar que toda vez que um id não puder ser convertido com sucesso isso implica que esta ordem não existirá, já que estabelecemos que nossas ordem serão sempre gravadas com o formato UUID, assim podemos “tranquilamente” retornar nossa exception personalizada OrderNotFoundException, que fatalmente irá implicar no retorno de um StatusCode 404, o que nos leva ao sucesso do nosso sexto caso de teste.

Não se esqueça de refatorar os demais métodos da controller que também faziam conversão do UUID, assim garantimos o mesmo funcionamento a todos.

Colocando o projeto para rodar

Note que até o momento toda aplicação foi construída sem ela de fato seja colocada em execução “nenhuma vez”(Na verdade toda vez que executamos o testes a aplicação de fato é executado, mas dentro de container controlado). Chegou a hora de colocar a aplicação em execução de fato e ver se toda nossa tentativa de criar uma aplicação com base primeiramente em testes funcionou.

Para isso vale lembra que inicialmente dissemos que nosso banco de dados de fato seria o Postgres, então vamos começar colocado ele para executar. Aqui faça como preferir, eu usarei o docker, e um arquio docker-compose para iniciar uma instancia localmente, vou compartilhar o arquivo, mas não vou entrar em detalhes sobre este passo. Basicamente criei uma nova pasta docker na raiz do projeto e dentro dela criei o arquivo:

Testes integrados com Spring

version: '3'

services:
  postgres:
    image: postgres:14
    container_name: postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
로그인 후 복사

Para quem conhece basta entrar no seu console favorito, navegar até a pasta e digitar o comando (Claro, você precisa do docker instalado na sua maquina):

docker compose up

Após digitar o comando ele deverá subir uma instancia de postgres na sua maquina com sucesso:

Testes integrados com Spring

Agora precisamos apontar nosso aplicação para conectar com o banco postgres, tal como fizemos com o ambiente de teste. Para isso dentro da pasta resource iremos encontrar um arquivo application.properties, somente por gosto pessoal iremos renomeá-lo para application.yaml (basicamente, o yaml é mesmos, verbosos e não precisamos repetir algumas coisas vária vezes). Uma vez renomeado iremos substituir conteúdo dele pelo seguinte:

spring:
  application:
  name: integration-test
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/postgres?currentSchema=public
    username: postgres
    password: postgres
  jpa:
    ddl-auto: update
    show-sql: true
로그인 후 복사

Com isso agora podemos rodar nossa aplicação, aqui vamos usar a facilidade do Intellij, indo ao até classe IntegrationtestApplication e clicando no botão:

Testes integrados com Spring

Pronto agora pode fazer um curl no console e ver que a aplicação funcionou com sucesso:

Testes integrados com Spring

Os fontes para o projeto completo está disponível no Github

https://github.com/marcosfaneli/integrationtest

위 내용은 Spring과의 통합 테스트의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!