Unit testing is the bedrock of robust software. When it comes to testing your Spring Boot applications that interact with a database, spinning up a full-fledged database instance for every test can be time-consuming and resource-intensive. This is where in-memory databases like H3 (HyperSQL Database Engine) shine.
H3 is a lightweight, pure Java SQL database that runs entirely in memory. This makes it incredibly fast for unit tests, as there’s no disk I/O involved. It supports standard SQL and integrates seamlessly with Spring Boot and Spring Data JPA, making it an excellent choice for isolating your data access logic during unit testing.
This article will guide you through using H3 for your Spring Boot unit tests, covering initialization, data setup, and teardown.
Why Choose H3 for Unit Testing?
- Blazing Fast: Runs in memory, significantly reducing test execution time.
- Lightweight: Minimal overhead and easy to set up.
- Pure Java: Platform-independent and requires no external dependencies beyond its JAR.
- Standard SQL Support: Familiar SQL syntax makes it easy to work with.
- Spring Boot Integration: Effortless configuration with Spring Boot’s testing features.
- Isolation: Each test can have its own isolated database instance, preventing data contamination between tests.
1. Adding the H3 Dependency
First, you need to include the H3 database dependency in your pom.xml
(for Maven) or build.gradle
(for Gradle) file:
Maven (pom.xml
):
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
Gradle (build.gradle
):
testImplementation 'com.h2database:h2'
The test
scope ensures that the H3 dependency is only included during testing and not in your production build.
2. Configuring H3 for Unit Tests in Spring Boot
Spring Boot automatically configures an embedded in-memory database if it finds one on the classpath and no explicit database configuration is provided. By default, it might choose H2 or Derby if both are present. To ensure H3 is used, you can explicitly configure it in your application.properties
or application.yml
within your src/test/resources
directory.
application.properties
:
spring.datasource.url=jdbc:h2:mem:unittestdb;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
application.yml
:
spring:
datasource:
url: jdbc:h2:mem:unittestdb;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
Explanation of the Configuration:
spring.datasource.url=jdbc:h2:mem:unittestdb;DB_CLOSE_DELAY=-1
: This is the JDBC URL for an in-memory H3 database namedunittestdb
.mem:
: Specifies an in-memory database.unittestdb
: The name you choose for your test database. Each test can potentially use a different name for isolation.DB_CLOSE_DELAY=-1
: This crucial setting prevents the in-memory database from being closed when the last connection is closed, ensuring it remains available for the duration of your test context.
spring.datasource.driver-class-name=org.h2.Driver
: Specifies the JDBC driver class for H3.spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
: Tells Hibernate (if you’re using Spring Data JPA) to use the H3-specific SQL dialect.spring.jpa.hibernate.ddl-auto=create-drop
: This Hibernate property automatically creates the database schema based on your JPA entities when the application context starts and drops it when the context closes. This is very convenient for unit tests as it ensures a clean database for each test run.
3. Setting Up Data for Your Tests
You have several options for setting up data in your H3 database for unit testing:
a) Using @Sql
Annotation (Spring Test):
The @Sql
annotation from Spring Test allows you to execute SQL scripts before and/or after your test methods or classes. This is a clean and declarative way to populate your database with test data.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;
import com.example.model.Product;
import com.example.repository.ProductRepository;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@Sql(scripts = "/import.sql") // Executes import.sql before the test class
public class ProductRepositoryH3SqlAnnotationTest {
@Autowired
private ProductRepository productRepository;
@Test
@Sql(scripts = "/insert_product.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void testFindByName() {
List<Product> products = productRepository.findByName("Test Product");
assertThat(products).hasSize(1);
assertThat(products.get(0).getPrice()).isEqualTo(25.0);
}
@Test
void testFindByPriceGreaterThan() {
List<Product> products = productRepository.findByPriceGreaterThan(20.0);
assertThat(products).hasSize(2); // Assuming import.sql inserts more than one product
}
}
- Create SQL script files (e.g.,
import.sql
,insert_product.sql
) in yoursrc/test/resources
directory containing your data setup SQL statements (INSERT statements). - Use
@Sql(scripts = "/path/to/your/script.sql")
at the class or method level to specify the script to execute. executionPhase
allows you to control when the script is executed (e.g.,BEFORE_TEST_METHOD
,AFTER_TEST_METHOD
).
b) Using @BeforeEach
with Repository save()
Methods:
You can programmatically set up data within your test methods or using @BeforeEach
to execute before each test. This is useful for more dynamic or test-specific data.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import com.example.model.Product;
import com.example.repository.ProductRepository;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
public class ProductRepositoryH3BeforeEachTest {
@Autowired
private ProductRepository productRepository;
@BeforeEach
void setUp() {
productRepository.save(new Product("Product A", 20.0));
productRepository.save(new Product("Product B", 30.0));
productRepository.save(new Product("Test Product", 25.0));
}
@Test
void testFindByName() {
List<Product> products = productRepository.findByName("Test Product");
assertThat(products).hasSize(1);
assertThat(products.get(0).getPrice()).isEqualTo(25.0);
}
@Test
void testFindByPriceGreaterThan() {
List<Product> products = productRepository.findByPriceGreaterThan(20.0);
assertThat(products).hasSize(2);
}
}
c) Using Spring Data JPA’s saveAll()
:
For inserting multiple records at once, you can use the saveAll()
method in your repository within a @BeforeEach
block.
@BeforeEach
void setUp() {
productRepository.saveAll(List.of(
new Product("Product A", 20.0),
new Product("Product B", 30.0),
new Product("Test Product", 25.0)
));
}
4. Teardown and Clean Up
With H3 and the spring.jpa.hibernate.ddl-auto=create-drop
configuration, the database schema is automatically dropped after your test context closes. This provides a clean slate for each test run without requiring explicit teardown in most cases.
However, if you need more granular control or want to clean up specific data after a test method, you can use:
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
: Create acleanup.sql
script withDELETE
statements.@AfterEach
with RepositorydeleteAll()
or specificdelete()
methods: Programmatically delete data after each test.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import com.example.model.Product;
import com.example.repository.ProductRepository;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
public class ProductRepositoryH3TeardownTest {
@Autowired
private ProductRepository productRepository;
@BeforeEach
void setUp() {
productRepository.save(new Product("Test Product", 25.0));
}
@Test
void testFindByName() {
List<Product> products = productRepository.findByName("Test Product");
assertThat(products).hasSize(1);
}
@AfterEach
void tearDown() {
productRepository.deleteAll(); // Clears all data after each test
}
}
Best Practices for Unit Testing with H3:
- Keep Tests Focused: Each unit test should verify a specific behavior of your data access logic.
- Use Meaningful Data: Create test data that clearly demonstrates the scenario you are testing.
- Ensure Test Isolation: While
create-drop
helps, be mindful of potential shared state if you reuse the same database name across tests without proper setup/teardown. Consider using unique database names or cleaning up data explicitly. - Test Edge Cases: Don’t just test the happy path. Include tests for empty results, null values, and boundary conditions.
- Verify State: Focus on asserting the state of your entities after repository operations.
Conclusion:
H3 is a powerful ally in your Spring Boot unit testing strategy. Its speed and ease of integration make it ideal for isolating and verifying your data access layer. By following the steps outlined in this article for initialization, data setup using @Sql
or programmatic methods, and understanding the automatic teardown provided by Spring Boot and H3, you can write faster, more reliable, and more maintainable unit tests for your Spring Data REST applications. Embrace the speed of in-memory testing and elevate the quality of your code!
Discover more from GhostProgrammer - Jeff Miller
Subscribe to get the latest posts sent to your email.