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)

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>
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)