Enterprise Java

Tear Down HSQLDB Database After Tests

Testing is a critical aspect of software development, and maintaining a clean test environment ensures accurate and reliable results. This article explores how to effectively manage database states in tests, focusing on how to set up and tear down Java HSQLDB databases.

The article explores the following solutions:

  • Using spring.jpa.hibernate.ddl-auto and @DirtiesContext to reload the application context.
  • Using @Sql annotation to reset the database state after every test with custom SQL scripts.

1. Setting Up the Test Environment

To set up the test environment, include the necessary dependencies in your pom.xml, such as spring-boot-starter-data-jpa, spring-boot-starter-test, and hsqldb. Then, configure the in-memory HSQLDB database in the application.properties file to ensure proper integration with your Spring application.

spring.datasource.url=jdbc:hsqldb:mem:testdb
spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true

The spring.jpa.hibernate.ddl-auto=create-drop ensures the database schema is recreated for each test suite.

Example Entity and Repository

A basic Employee entity and repository will be used as examples.

@Entity
@Table(name = "EMPLOYEE")
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 37 * hash + Objects.hashCode(this.id);
        hash = 37 * hash + Objects.hashCode(this.name);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Employee other = (Employee) obj;
        if (!Objects.equals(this.name, other.name)) {
            return false;
        }
        return Objects.equals(this.id, other.id);
    }
        
}

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    
    Optional<Employee> findByName(String name);
}

2. Using hibernate.hbm2ddl.auto and @DirtiesContext

This approach uses the hibernate.hbm2ddl.auto property to recreate the schema on application startup and the @DirtiesContext annotation to reload the Spring context after each test.

@SpringBootTest
public class EmployeeServiceDirtiesContextTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private EmployeeRepository employeeRepository;

    @BeforeEach
    void setup() {
        // Insert any required initial data
        String insertSql = "INSERT INTO EMPLOYEE (id, name) VALUES (?, ?)";
        Employee employee = new Employee();
        employee.setId(1L);
        employee.setName("JCG User");
        jdbcTemplate.update(insertSql, employee.getId(), employee.getName());
    }

    @AfterEach
    void cleanup() {
        // Clean up all data
        jdbcTemplate.execute("TRUNCATE TABLE EMPLOYEE");
    }

    @Test
    @DirtiesContext
    void givenNewEmployee_whenSaved_thenCanBeFound() {
        Employee employee = new Employee();
        employee.setId(1L);
        employee.setName("John Doe");

        Employee newEmployee = employeeRepository.save(employee);

        Assertions.assertEquals(employee, newEmployee);
      
    }

    @Test
    @DirtiesContext     
    void givenExistingEmployee_whenFoundByName_thenReturnEmployee() {
        Optional<Employee> employee = employeeRepository.findByName("JCG User");

        assertTrue(employee.isPresent(), "Employee should be found");
        assertEquals("JCG User", employee.get().getName(), "The employee does match the expected employee.");
    }

    @Test
    @DirtiesContext
    void givenNonExistingEmployee_whenFoundByName_thenReturnNull() {
        Optional<Employee> employee = employeeRepository.findByName("John Doe");

        assertFalse(employee.isPresent(), "Employee should not be found");
    }
}


This class uses @SpringBootTest to initialize the entire Spring context, injecting the JdbcTemplate for direct database operations. The @BeforeEach method populates the EMPLOYEE table with initial data before each test, while the @AfterEach method truncates the table after each test to ensure isolation.

The class contains three test cases. The givenNewEmployee_whenSaved_thenCanBeFound test ensures that saving a new employee to the repository works as expected.

The givenExistingEmployee_whenFoundByName_thenReturnEmployee test verifies that an existing employee in the database can be retrieved by name.

Finally, the givenNonExistingEmployee_whenFoundByName_thenReturnNull test confirms that searching for a non-existing employee by name correctly returns no results.

The use of @DirtiesContext ensures that the application context is refreshed after each test, maintaining the reliability of the tests by isolating any context-level changes.

3. Using @Sql Annotation with Custom SQL Scripts

The @Sql annotation provides a way to execute custom SQL scripts before or after each test. This approach is useful for cleaning up the database after every test to ensure data isolation and maintain a consistent state. For example, a script like clean_file.sql can be used to define SQL statements that reset or clear the database after a test completes.

SQL Script (src/main/resources/clean_file.sql)

TRUNCATE TABLE EMPLOYEE;

Test Example

@SpringBootTest
@DirtiesContext
public class EmployeeServiceSQLCleanUpTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private EmployeeRepository employeeRepository;

    @BeforeEach
    void setup() {
        // Insert any required initial data
        String insertSql = "INSERT INTO EMPLOYEE (id, name) VALUES (?, ?)";
        Employee employee = new Employee();
        employee.setId(1L);
        employee.setName("JCG User");
        jdbcTemplate.update(insertSql, employee.getId(), employee.getName());
    }

    @Test
    @Sql(scripts = "/clean_file.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public void givenNewEmployee_whenSaved_thenCanBeFound() {
        Employee employee = new Employee();
        employee.setId(2L);
        employee.setName("John Doe");

        Employee newEmployee = employeeRepository.save(employee);

        Assertions.assertEquals(employee, newEmployee);

    }

    @Test
    @Sql(scripts = "/clean_file.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public void givenExistingEmployee_whenFoundByName_thenReturnEmployee() {
        Optional<Employee> employee = employeeRepository.findByName("JCG User");

        assertTrue(employee.isPresent(), "Employee should be found");
        assertEquals("JCG User", employee.get().getName(), "The employee does match the expected employee.");
    }

    @Test
    @Sql(scripts = "/clean_file.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public void givenNonExistingEmployee_whenFoundByName_thenReturnNull() {
        Optional<Employee> employee = employeeRepository.findByName("John Doe");

        assertFalse(employee.isPresent(), "Employee should not be found");
    }

}

The above EmployeeServiceSQLCleanUpTest class demonstrates how to use the @Sql annotation to clean up the database after each test. The test cases validate various scenarios of the EmployeeRepository. The use of @Sql with a cleanup script (/clean_file.sql) after each test method guarantees a clean database state, avoiding data interference across tests.

When the application is executed, the test suite runs all the defined test cases, ensuring the database behaves as expected. For each test, the database is set up with predefined data, and after execution, it is cleaned up using the @Sql script.

4. Conclusion

In this article, we explored various approaches to tear down or reset an HSQLDB database after tests in a Spring application using Hibernate, JPA, and JUnit. From leveraging hibernate.hbm2ddl.auto with @DirtiesContext for schema recreation to employing @Sql annotations with custom scripts for flexible cleanup, each method offers unique benefits tailored to specific use cases.

5. Download the Source Code

This article covered how to tear down an HSQLDB database in Java.

Download
You can download the full source code of this example here: Java hsqldb tear down

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button