1. Introdução
Se você chegou até aqui, provavelmente quer entender como fazer testes de integração com Spring Boot, usando @MockBean ou um banco de dados em memória como o H2.
A diferença entre essas abordagens é simples:
- Com @MockBean, você simula componentes específicos, retornando valores pré-definidos.
- Com H2, você executa os testes em um banco em memória que simula o comportamento do banco real.
Antes de seguir, é importante entender duas anotações do Spring Boot:
-
@WebMvcTest
: ideal para testes unitários de controllers, sem carregar todo o contexto da aplicação. -
@SpringBootTest
: carrega o contexto completo da aplicação, sendo o que usaremos aqui.
Observações importantes:
- o projeto evita o uso de
@Service
,@Repository
e@Autowired
, a injeção de dependência é feita de forma manual; - há links no fim do artigo explicando os motivos para evitar as notações citadas e sobre os tipos de testes.
Descubra como escrever testes de integração eficazes com Spring Boot usando duas abordagens poderosas: banco de dados em memória com H2 e simulações com @MockBean
. Nesta postagem, será mostrado como configurar cada cenário com exemplos reais, boas práticas e dicas práticas para evitar armadilhas comuns. Testar nunca foi tão direto ao ponto.
Então, vamos lá!?
2. Visão Geral do Projeto
2.1. Dependências do Maven (pom.xml
)
O pom.xml
está configurado com dependências para Spring Boot, JPA, PostgreSQL, H2, Lombok e bibliotecas de teste.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.javawebtest</groupId> <artifactId>java-web-test</artifactId> <version>0.0.1-SNAPSHOT</version> <name>java-web-test</name> <description>Project to test anything about java web</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-docker-compose</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </path> </annotationProcessorPaths> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok-mapstruct-binding</artifactId> <version>0.2.0</version> </dependency> </annotationProcessorPaths> <compilerArgs> <compilerArg> -Amapstruct.defaultComponentModel=spring </compilerArg> </compilerArgs> </configuration> </plugin> </plugins> </build> </project>
Nota: verifique as configurações do plugin do Lombok caso a aplicação não inicie corretamente.
2.2. Configuração dos @Bean
O projeto utiliza uma classe com @Configuration
para definir os @Bean
principais:
@Configuration public class ProductConfiguration { public static final String PRODUCT_REPOSITORY_BEAN = "productRepositoryBean"; public static final String PRODUCT_SERVICE_BEAN = "productServiceBean"; @Profile({"!test"}) @Bean(PRODUCT_REPOSITORY_BEAN) public ProductRepository productRepository(ProductJpaRepository productJpaRepository) { return new ProductRepositoryImpl(productJpaRepository); } @Bean(PRODUCT_SERVICE_BEAN) public ProductService productService(@Qualifier(PRODUCT_REPOSITORY_BEAN) ProductRepository productRepository) { return new ProductServiceImpl(productRepository); } }
Atenção a notação @Profile({"!test"})
, aqui é informado que quando o profile a ser executado contiver o valor teste esse @Bean
deverá ficar sem ser ativado. Isso é importante para exemplificar o ponto aqui a ser passado.
2.3. Estrutura de Classes
O projeto possui:
-
Product
: entidade
@JsonInclude(JsonInclude.Include.NON_EMPTY) @Data @Builder @AllArgsConstructor @NoArgsConstructor @Entity(name = "product") @Table(name = "product") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "name") private String name; @Column(name = "creation_date") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") private LocalDateTime creationDate; }
-
ProductController
: API Rest
@EnableWebMvc @RestController @RequestMapping(value = "/product") public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } @PostMapping(value = {"", "/"}, produces = {MediaType.APPLICATION_JSON_VALUE}, consumes = {MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity<Product> save (@RequestBody Product product) { product = productService.save(product); return ResponseEntity.status(HttpStatus.CREATED).body(product); } }
-
ProductService
: lógica de negócio
public interface ProductService { Product save(Product product); } public class ProductServiceImpl implements ProductService { private final ProductRepository productRepository; public ProductServiceImpl(ProductRepository productRepository) { this.productRepository = productRepository; } @Override public Product save(Product product) { product = productRepository.save(product); return product; } }
-
ProductRepository
: camada de persistência
public interface ProductJpaRepository extends JpaRepository<Product, Long> { } public interface ProductRepository { Product save(Product product); } public class ProductRepositoryImpl implements ProductRepository { private final ProductJpaRepository productJpaRepository; public ProductRepositoryImpl(ProductJpaRepository productJpaRepository) { this.productJpaRepository = productJpaRepository; } @Override public Product save(Product product) { product = productJpaRepository.save(product); return product; } }
2.4. Fluxo da Aplicação
O fluxo é Controller → Service → Repository → Banco de Dados
.
2.5. application.yml
(ambiente dev)
Quando se trabalha com Springboot uma das formas de se configurar as variáveis do projeto é através do arquivo application.yml, sendo assim, a configuração do banco de dados também fica neste arquivo, segue um exemplo da configuração do projeto.
spring: config: activate: on-profile: dev jpa: defer-datasource-initialization: true show-sql: true datasource: url: jdbc:postgresql://localhost:5432/test username: test password: test1234 driverClassName: org.postgresql.Driver server: port: 8080
3. Teste de Integração com H2
Ao realizar testes, é comum substituir o banco real por um banco em memória como o H2. Para isso:
3.1. application.yml
para testes
Arquivo localizado em src/test/resources/
:
spring: config: activate: on-profile: test jpa: defer-datasource-initialization: true show-sql: true hibernate: ddl-auto: create-drop h2: console: enabled: true datasource: url: jdbc:h2:mem:test;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE username: sa password: driverClassName: org.h2.Driver
3.2. Configuração dos @Bean
para teste
Assim como em produção os @Bean
deverão ser mapeados, no caso em tela, o @Bean
relativo ao banco de dados deverá ser mapeado também. Repare que a notação mudou de @Configuration
para @TestConfiguration
.
@TestConfiguration public class ConfigTest { @Profile({"test"}) @Bean(ProductConfiguration.PRODUCT_REPOSITORY_BEAN) public ProductRepository productRepository(ProductJpaRepository productJpaRepository) { return new ProductRepositoryImpl(productJpaRepository); } }
Atenção a notação @Profile({"test"})
, aqui é informado que quando o profile a ser executado contiver o valor teste esse @Bean
deverá ser ativado. Isso é importante para exemplificar o ponto aqui a ser passado.
3.3. O Teste
O ponto mais esperado chegou, o momento de criar o teste e vê-lo funcionar.
@ActiveProfiles({"test"}) @SpringBootTest(classes = {ConfigTest.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class ProductControllerH2Test { @Autowired MockMvc mockMvc; ObjectMapper objectMapper; @BeforeEach void beforeEach() { objectMapper = new ObjectMapper(); } @Test public void test() throws Exception { String body = objectMapper.writeValueAsString(Product.builder() .name("new product") .build()); mockMvc.perform(MockMvcRequestBuilders.post("/product/") .content(body) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("new product")) .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1)); } }
Neste ponto é importante observar algumas notações, tais quais @ActiveProfiles({"test"})
e
@SpringBootTest
. A notação @ActiveProfiles({"test"})
indica que para executar os testes desta classe deverá ser ativado o profile test, desta forma, excluindo outras configurações. Sobre a notação @SpringBootTest
foi explicado anteriormente sobre a sua função, no entanto, tem algo especificado dentro dela que é importante, que é classes = {ConfigTest.class}
, isso significa que a classe ConfigTest.class
deverá ser carregada no contexto do Spring quando o mesmo for inicializado.
Repare que inexiste notações de mock para os @Bean
do projeto, isso ocorreu pois de fato nada está sendo “mockado”, o fluxo ocorrerá do início ao fim, porém, o banco de dados utilizado será o H2 ao invés do banco utilizado em prod, dev, homolog ou algo do tipo que poderia ser um MySQL, PostgreSQL, Oracle ou outro.
4. Teste de Integração com @MockBean
Aqui, ao invés de usar um banco em memória, utilizamos mocks para simular o comportamento do repositório.
@ActiveProfiles({"test"}) @SpringBootTest(classes = {ConfigTest.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class ProductControllerMockTest { @Autowired MockMvc mockMvc; ObjectMapper objectMapper; @MockBean ProductJpaRepository productJpaRepository; @BeforeEach void beforeEach() { objectMapper = new ObjectMapper(); } @Test public void test() throws Exception { Product product = Product.builder() .name("new product") .build(); Mockito.when(productJpaRepository.save(product)) .thenReturn(Product.builder() .id(1L) .name("test mock productJpaRepository") .build()); String body = objectMapper.writeValueAsString(product); mockMvc.perform(MockMvcRequestBuilders.post("/product/") .content(body) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("test mock productJpaRepository")) .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1)); } }
Atenção: o @MockBean
precisa ser usado no ponto certo do fluxo. Mockar classes fora da cadeia de execução esperada pode quebrar a injeção de dependências.
5. Quando usar H2 ou @MockBean?
Critério | H2 (Banco em Memória) | @MockBean (Simulação de Dependências) |
---|---|---|
Fidelidade com banco real | ✅ Alta (simula o banco de verdade com SQL real) | ❌ Baixa (lógica simulada, sem persistência real) |
Performance | ❌ Mais lento (carrega contexto + banco em memória) | ✅ Rápido (sem overhead de banco) |
Cobertura de integração | ✅ Completa (inclui JPA, transações, etc.) | ❌ Limitada (testa só partes isoladas) |
Manutenção de testes | ⚠️ Média (precisa manter schema e dados consistentes) | ✅ Simples (mocka só o necessário) |
Diagnóstico de bugs reais | ✅ Melhor (simula situações reais de persistência) | ❌ Pode mascarar bugs que só aparecem com DB real |
Complexidade de setup | ❌ Alta (requires profile, config H2, beans específicos) | ✅ Baixa (só anotar com @MockBean e boa) |
Indicado para testes de... | 🔍 Integração ponta-a-ponta, fluxo completo | 🧪 Lógica de negócio, regras isoladas |
Depende de schema do banco? | ✅ Sim | ❌ Não |
Ideal para TDD? | ❌ Não (mais lento e complexo) | ✅ Sim (rápido e direto ao ponto) |
Pode-se considerar então que:
- use H2 se quiser testar o comportamento completo da aplicação, incluindo a persistência real de dados.
- use @MockBean para testar lógica de negócio de forma isolada, sem depender da infraestrutura.
Ambas técnicas são válidas e, inclusive, complementares.
6. Links de Referência
- Tipos de testes: unitário, integração, E2E
- Injeção de dependência com @Autowired
- Anotações do Spring: @Component, @Service, @Repository
- Problemas com o uso de @Autowired
Se você leu até aqui, parabéns: agora tem munição suficiente pra escrever testes robustos no Spring Boot. Se não escrever, o bug vai te pegar! 😂
Top comments (0)