Pular para o conteúdo principal

Aula 6: Testes Automatizados com Quarkus

Objetivos

  • Compreender a pirâmide de testes e os diferentes níveis de verificação
  • Implementar testes unitários com JUnit 5 e Mockito
  • Criar testes de integração REST com REST Assured e @InjectMock
  • Utilizar DevServices para testes com banco de dados real
  • Construir uma suíte de testes completa cobrindo repositório, serviço e recurso REST

Contexto e Motivação

Testes automatizados são o "airbag" do desenvolvimento profissional de software. Assim como um carro moderno não é seguro sem sistemas de proteção, um sistema sem testes está vulnerável a falhas inesperadas e regressões. Testar não é apenas encontrar erros, mas garantir que o software evolua com confiança e qualidade.

observação

Em aulas anteriores, aprendemos CDI para injeção de dependências e JAX-RS para APIs REST. Essas especificações são exatamente o que torna possível testar camadas isoladamente — CDI permite substituir beans reais por mocks, e JAX-RS permite simular requisições HTTP.

Por que testes são inegociáveis?

BenefícioImpacto
Prevenção de regressõesMudanças futuras não quebram funcionalidades existentes
Facilitam refatoraçõesPermite melhorar o código sem medo de introduzir bugs
Documentam comportamentoTestes servem como exemplos vivos de uso das APIs
Aumentam confiançaEquipes entregam mais rápido e com menos erros
Reduzem custoBug encontrado em teste custa 10-100x menos que em produção

A Pirâmide de Testes

NívelVelocidadeEscopoFerramenta no Quarkus
UnitárioMilissegundosUma classe/métodoJUnit 5 + Mockito
IntegraçãoSegundosMúltiplas camadas@QuarkusTest + REST Assured
E2EMinutosSistema completoSelenium, Playwright

Conteúdo

Ferramentas de Teste no Quarkus

O Quarkus oferece suporte nativo para testes com ferramentas modernas:

  • JUnit 5: Framework base para organização e execução de testes
  • REST Assured: DSL fluente para testes de APIs HTTP
  • Mockito: Criação de mocks e stubs para dependências
  • @QuarkusTest: Inicia o container Quarkus para testes de integração
  • @InjectMock: Substitui beans CDI por mocks em testes de integração
  • DevServices: Provisiona bancos de dados automaticamente via Docker para testes

Dependências necessárias (já incluídas em projetos Quarkus):

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>

Testes Unitários com JUnit 5

Testes unitários verificam uma única unidade de código (classe/método) de forma isolada. Não dependem de container, banco de dados ou rede.

package br.upf.ads175.repository;

import br.upf.ads175.dto.ProdutoDTO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("ProdutoRepository - Testes Unitários")
class ProdutoRepositoryTest {

private ProdutoRepository repository;

@BeforeEach // Executa antes de cada método de teste
void setUp() {
repository = new ProdutoRepository(); // Teste unitário puro, sem CDI
}

@Test
@DisplayName("Deve encontrar produto por ID existente")
void deveEncontrarProdutoPorIdExistente() {
// Given — Dado um ID que sabemos que existe
Long idExistente = 1L;

// When — Quando buscamos por esse ID
Optional<ProdutoDTO> resultado = repository.findById(idExistente);

// Then — Então deve retornar o produto correto
assertTrue(resultado.isPresent(), "Produto deveria ser encontrado");
assertEquals(idExistente, resultado.get().id(), "ID deve ser o esperado");
}

@Test
@DisplayName("Deve retornar Optional vazio para ID inexistente")
void deveRetornarVazioParaIdInexistente() {
Optional<ProdutoDTO> resultado = repository.findById(999L);
assertTrue(resultado.isEmpty(), "Produto não deveria ser encontrado");
}

@Test
@DisplayName("Deve retornar apenas produtos ativos")
void deveRetornarApenasProdutosAtivos() {
List<ProdutoDTO> produtosAtivos = repository.findByAtivo(true);
assertFalse(produtosAtivos.isEmpty(), "Lista não deve estar vazia");
assertTrue(produtosAtivos.stream().allMatch(ProdutoDTO::ativo),
"Todos os produtos devem estar ativos");
}

@Test
@DisplayName("Deve retornar produtos dentro da faixa de preço")
void deveRetornarProdutosDentroFaixaPreco() {
BigDecimal precoMin = new BigDecimal("100.00");
BigDecimal precoMax = new BigDecimal("1000.00");

List<ProdutoDTO> produtos = repository.findByPrecoEntre(precoMin, precoMax);

assertFalse(produtos.isEmpty(), "Lista não deve estar vazia");
assertTrue(produtos.stream()
.allMatch(p -> p.preco().compareTo(precoMin) >= 0
&& p.preco().compareTo(precoMax) <= 0),
"Todos devem estar na faixa de preço");
}
}
observação

Note que criamos uma instância direta do repositório com new. Isso é um teste unitário puro, sem envolver o container CDI. Para testes de integração, usaremos @QuarkusTest.


Testes com Mocks (Mockito)

Quando uma classe tem dependências, usamos mocks para simular o comportamento das dependências e testar apenas a lógica da classe sob teste.

package br.upf.ads175.service;

import br.upf.ads175.dto.CategoriaDTO;
import br.upf.ads175.dto.ProdutoDTO;
import br.upf.ads175.repository.ProdutoRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class) // Integra Mockito com JUnit 5
@DisplayName("ProdutoService - Testes Unitários com Mocks")
class ProdutoServiceTest {

@Mock // Cria um mock do repositório
ProdutoRepository repository;

@InjectMocks // Cria o serviço e injeta os mocks
ProdutoService service;

private List<ProdutoDTO> produtosMock;

@BeforeEach
void setUp() {
produtosMock = List.of(
new ProdutoDTO(1L, "Produto A", new BigDecimal("500.00"), true, new CategoriaDTO("Cat1")),
new ProdutoDTO(2L, "Produto B", new BigDecimal("1500.00"), true, new CategoriaDTO("Cat2")),
new ProdutoDTO(3L, "Produto C", new BigDecimal("300.00"), false, new CategoriaDTO("Cat1"))
);
}

@Test
@DisplayName("Deve retornar produtos ativos ordenados por nome")
void deveRetornarProdutosAtivosOrdenadosPorNome() {
// Given — Mock retorna produtos ativos
when(repository.findByAtivo(true)).thenReturn(
produtosMock.stream().filter(ProdutoDTO::ativo).toList()
);

// When
List<ProdutoDTO> resultado = service.buscarProdutosAtivosOrdenadosPorNome();

// Then
assertFalse(resultado.isEmpty(), "Lista não deve estar vazia");
assertTrue(resultado.stream().allMatch(ProdutoDTO::ativo), "Todos devem estar ativos");
assertEquals("Produto A", resultado.get(0).nome());
assertEquals("Produto B", resultado.get(1).nome());

// Verifica que o repositório foi chamado exatamente 1 vez
verify(repository, times(1)).findByAtivo(true);
}

@Test
@DisplayName("Deve retornar produto quando ID existe")
void deveRetornarProdutoQuandoIdExiste() {
Long idExistente = 1L;
ProdutoDTO produtoEsperado = produtosMock.get(0);
when(repository.findById(idExistente)).thenReturn(Optional.of(produtoEsperado));

Optional<ProdutoDTO> resultado = service.buscarPorId(idExistente);

assertTrue(resultado.isPresent(), "Produto deve ser encontrado");
assertEquals(produtoEsperado.id(), resultado.get().id());
verify(repository, times(1)).findById(idExistente);
}

@Test
@DisplayName("Deve retornar Optional vazio quando ID não existe")
void deveRetornarVazioQuandoIdNaoExiste() {
when(repository.findById(999L)).thenReturn(Optional.empty());

Optional<ProdutoDTO> resultado = service.buscarPorId(999L);

assertTrue(resultado.isEmpty(), "Resultado deve estar vazio");
verify(repository, times(1)).findById(999L);
}
}
important

Esta é a diferença fundamental entre teste unitário e de integração: aqui testamos apenas a lógica do serviço, mockando suas dependências. Se um teste falhar, sabemos que o problema está na lógica do serviço, não no repositório.


Testes de Integração REST com @QuarkusTest

Testes de integração verificam a interação entre múltiplas camadas. O @QuarkusTest inicia o container Quarkus real:

package br.upf.ads175.resource;

import br.upf.ads175.dto.CategoriaDTO;
import br.upf.ads175.dto.ProdutoDTO;
import br.upf.ads175.service.ProdutoService;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.InjectMock;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;

@QuarkusTest // Inicia o Quarkus para testes de integração
@DisplayName("ProdutoResource - Testes de Integração REST")
class ProdutoResourceTest {

@InjectMock // Mock do serviço no contexto CDI
ProdutoService produtoService;

private List<ProdutoDTO> produtosMock = List.of(
new ProdutoDTO(1L, "Notebook Gamer", new BigDecimal("8500.00"), true, new CategoriaDTO("Eletrônicos")),
new ProdutoDTO(2L, "Cadeira de Escritório", new BigDecimal("1200.50"), true, new CategoriaDTO("Móveis"))
);

@Test
@DisplayName("GET /produtos deve retornar lista de produtos ativos")
void deveRetornarListaProdutosAtivos() {
when(produtoService.buscarProdutosAtivosOrdenadosPorNome()).thenReturn(produtosMock);

given()
.when()
.get("/produtos")
.then()
.statusCode(200)
.contentType("application/json")
.body("$", hasSize(2))
.body("[0].id", equalTo(1))
.body("[0].nome", equalTo("Notebook Gamer"));

verify(produtoService, times(1)).buscarProdutosAtivosOrdenadosPorNome();
}

@Test
@DisplayName("GET /produtos/{id} deve retornar 404 quando produto não existe")
void deveRetornar404QuandoProdutoNaoExiste() {
when(produtoService.buscarPorId(999L)).thenReturn(Optional.empty());

given()
.when()
.get("/produtos/{id}", 999)
.then()
.statusCode(404)
.contentType("application/json")
.body("erro", equalTo("Produto não encontrado"));

verify(produtoService, times(1)).buscarPorId(999L);
}

@Test
@DisplayName("GET /produtos/{id} deve retornar produto quando existe")
void deveRetornarProdutoQuandoIdExiste() {
ProdutoDTO produto = produtosMock.get(0);
when(produtoService.buscarPorId(1L)).thenReturn(Optional.of(produto));

given()
.when()
.get("/produtos/{id}", 1)
.then()
.statusCode(200)
.body("nome", equalTo("Notebook Gamer"))
.body("preco", equalTo(8500.00f));
}
}
observação

@QuarkusTest vs Teste Unitário: Aqui o Quarkus realmente inicia, o CDI funciona, e fazemos requisições HTTP reais. Mockamos apenas o serviço para focar no comportamento do endpoint.


Executando Testes

./mvnw test
dica

O Quarkus Dev Mode é perfeito para TDD (Test-Driven Development). Escreva um teste que falha, implemente o código para fazê-lo passar, e veja o resultado imediatamente — sem reiniciar nada.


Análise da Arquitetura de Testes

Exercícios (Checkpoints)

  1. Crie um teste unitário adicional para ProdutoRepository que valide a contagem de produtos por categoria (countByCategoria).
  2. Implemente uma validação de faixa de preço no serviço (buscarProdutosPorFaixaPreco) que lance IllegalArgumentException quando precoMin > precoMax, e escreva o teste correspondente.
  3. Crie um endpoint GET /produtos/categoria/{nome} e escreva testes de integração para ele (cenário com resultados e cenário sem resultados).
  4. Adicione um cenário de teste que simule falha do repositório (exceção) e verifique o comportamento do serviço.
  5. Pesquise sobre @QuarkusIntegrationTest e explique como ele difere do @QuarkusTest.

Referências