DEV Community

Cover image for Testes de Integração no Spring Boot: H2 vs @MockBean com exemplos reais
Mairon Costa
Mairon Costa

Posted on

Testes de Integração no Spring Boot: H2 vs @MockBean com exemplos reais

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)

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> 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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; } 
Enter fullscreen mode Exit fullscreen mode
  • 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); } } 
Enter fullscreen mode Exit fullscreen mode
  • 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; } } 
Enter fullscreen mode Exit fullscreen mode
  • 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; } } 
Enter fullscreen mode Exit fullscreen mode

2.4. Fluxo da Aplicação

O fluxo é Controller → Service → Repository → Banco de Dados.

Image description

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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)); } } 
Enter fullscreen mode Exit fullscreen mode

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)); } } 
Enter fullscreen mode Exit fullscreen mode

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


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)