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.
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ício | Impacto |
|---|---|
| Prevenção de regressões | Mudanças futuras não quebram funcionalidades existentes |
| Facilitam refatorações | Permite melhorar o código sem medo de introduzir bugs |
| Documentam comportamento | Testes servem como exemplos vivos de uso das APIs |
| Aumentam confiança | Equipes entregam mais rápido e com menos erros |
| Reduzem custo | Bug encontrado em teste custa 10-100x menos que em produção |
A Pirâmide de Testes
| Nível | Velocidade | Escopo | Ferramenta no Quarkus |
|---|---|---|---|
| Unitário | Milissegundos | Uma classe/método | JUnit 5 + Mockito |
| Integração | Segundos | Múltiplas camadas | @QuarkusTest + REST Assured |
| E2E | Minutos | Sistema completo | Selenium, 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");
}
}
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);
}
}
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));
}
}
@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
- Todos
- Específico
- Dev Mode
./mvnw test
./mvnw test -Dtest=ProdutoServiceTest
./mvnw quarkus:dev
# Pressione 'r' para executar testes
# Alterações no código re-executam automaticamente
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)
- Crie um teste unitário adicional para
ProdutoRepositoryque valide a contagem de produtos por categoria (countByCategoria). - Implemente uma validação de faixa de preço no serviço (
buscarProdutosPorFaixaPreco) que lanceIllegalArgumentExceptionquando precoMin > precoMax, e escreva o teste correspondente. - Crie um endpoint
GET /produtos/categoria/{nome}e escreva testes de integração para ele (cenário com resultados e cenário sem resultados). - Adicione um cenário de teste que simule falha do repositório (exceção) e verifique o comportamento do serviço.
- Pesquise sobre
@QuarkusIntegrationTeste explique como ele difere do@QuarkusTest.