Java integration test database

How to Write Integration Tests with H2 In-Memory Database and Springboot

If you have already worked on a professional application, you have had to do some unit testing or integration testing. Unfortunately, it can be intriguing at first to set up tests completely and functionally when you come to a new project.

This tutorial will show you how to set up integration tests with the H2 database.

What is in memory database

An in-memory database (IMDB) is a database that primarily relies on system memory for data storage instead of database management systems that employ a disk storage mechanism.

Because memory access is faster than disk access. We use the in-memory database when we do not need to persist the data. It is an embedded database. The in-memory databases are volatile by default, and all stored data is lost when we restart the application.

The widely used in-memory databases are H2, HSQLDB (HyperSQL Database), and Apache Derby. They create the configuration automatically.

H2 Database

H2 is an embedded, open-source, and in-memory database. It is a relational database management system written in Java. It is a client/server application. It is generally used for integration testing. It stores data in memory, not persist the data on disk.

Main features

  • Very fast database engine
  • Supports standard SQL, JDBC (Java Database Connectivity) API
  • Embedded and Server mode, Clustering support
  • Strong security features
  • The PostgreSQL ODBC (Open Database Connectivity) driver can be used
Читайте также:  Python pandas удалить дубликаты строк

Practice

Firstly, I am the first to forbid testing with an H2 database because it does not necessarily reflect the behaviour of databases in production. The way that H2 doesn’t reflect your production database significantly reduces the meaning and reliability of your tests.

In addition, a green test doesn’t guarantee that your code will work on a real-world database. Know that there will be differences in syntax and behaviour that I cannot explain in detail. Still, you have to deal with a legacy code in some cases.

Thus, you want to have (for example) an H2 Integration test implemented. That’s why I wrote this article.

However, maybe you are working on a new project and want to implement some good practices and work with Integration tests with Docker and Testcontainers; consider looking at this article below.

📢 If it’s not the case, you can continue reading this article on writing Integration Tests with H2 In-Memory Database.

Entity

For this demo project, let’s say you have a User table in your database.

@Getter @Entity @Builder @Table(name = "USER") @NoArgsConstructor @AllArgsConstructor public class User

Dependencies

To be able to use some methods in this demo project, you have to use all these additional dependencies.

  org.springframework.boot spring-boot-starter-data-jpa  com.h2database h2 runtime  org.projectlombok lombok 1.18.24 true  org.springframework.boot spring-boot-starter-test 2.7.3 test  org.testcontainers junit-jupiter 1.17.3 test  

Service

Now let’s define your service to handle some logic.

@Service @RequiredArgsConstructor public class UserService < private final UserRepository repository; public User saveOneUser(User user)< return this.repository.save(user); >public User findOneUser(int userId) < return this.repository.findById(userId).orElseThrow(EntityNotFoundException::new); >public List findAllUser() < return IterableUtils.toList(this.repository.findAll()) .stream() .sorted(Comparator.comparingLong(User::getId)) .collect(Collectors.toList()); >public void deleteOneUser(int userId) < this.repository.deleteById(userId); >> 

Knowing that integration tests only focus on the Controller part and if you ever wonder how to test the Service part of our system, here is an article below that might interest you.

Controller

Let’s define the HTTP Methods in your Controller class.

@RestController @RequestMapping("/user") @RequiredArgsConstructor public class UserController < private final UserService service; @PostMapping("/create") @ResponseStatus(HttpStatus.CREATED) public User createUser(@RequestBody User user) < return this.service.saveOneUser(user); >@GetMapping("/fetch/") @ResponseStatus(HttpStatus.OK) public User retrieveUser(@PathVariable int id) < return this.service.findOneUser(id); >@GetMapping("/fetchAll") @ResponseStatus(HttpStatus.OK) public List retrieveUsers() < return this.service.findAllUser(); >@DeleteMapping("delete/") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteOneUser(@PathVariable("id") int userId) < this.service.deleteOneUser(userId); >> 

Configuration

To launch our integration tests with the H2 database, we must configure our application-test.yml file.

#SPRING CONFIGURATION spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:demo;DB_CLOSE_ON_EXIT=FALSE username: sa password: jpa: hibernate: ddl-auto: create-drop defer-datasource-initialization: true show-sql: true properties: hibernate: dialect: H2Dialect format_sql: true #LOGGING CONFIGURATION logging: level: org: hibernate: sql: info 

Let’s configure some dummy datas for our In Memory H2 Database.
First we have to clear our fixtures data before each test method.

TRUNCATE TABLE USER RESTART IDENTITY; 

To be able to test the interactions with the database, we will create fixtures in init/user-data.sql to execute the CRUD requests

-- INSERT 5 DATA FIXTURES INSERT INTO USER (id, name, email) VALUES (1, 'Roger Clara', 'alicia.marty@gmail.com'); INSERT INTO USER (id, name, email) VALUES (2, 'Camille Guérin', 'rayan.sanchez@yahoo.fr'); INSERT INTO USER (id, name, email) VALUES (3, 'Julien Mael', 'laura.royer@yahoo.fr'); INSERT INTO USER (id, name, email) VALUES (4, 'Gérard Mael', 'victor.dupuis@hotmail.fr'); INSERT INTO USER (id, name, email) VALUES (5, 'Dubois Anaïs', 'alice.lemoine@hotmail.fr'); 

Create a json file init/user.json , that we will use to create a dummy user

Integration Test

Now you have to define your Integration Class

@SpringBootTest @AutoConfigureMockMvc @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class UserControllerTest
  • We use the annotation @SpringBootTest for bootstrapping the entire container. The annotation works by creating the ApplicationContexts that will be utilized in our Tests.
  • Then use @AutoConfigureMockMvc to our test class to enable and configure auto-configuration of Mock-Mvc.
  • Finally we use @DisplayNameGeneration to declare a custom display name generator for the annotated test class.

You will also have to define UserRepository and MockMvc to perform respective actions on InMemory Database and simulate real-world requests through our controller.

Let’s create our first integration test for user creation.

@Test @SqlGroup(< @Sql(value = "classpath:empty/reset.sql", executionPhase = BEFORE_TEST_METHOD), @Sql(value = "classpath:init/user-data.sql", executionPhase = BEFORE_TEST_METHOD) >) void should_create_one_user() throws Exception < final File jsonFile = new ClassPathResource("init/user.json").getFile(); final String userToCreate = Files.readString(jsonFile.toPath()); this.mockMvc.perform(post("/user/create") .contentType(APPLICATION_JSON) .content(userToCreate)) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath("$").isMap()) .andExpect(jsonPath("$", aMapWithSize(3))); assertThat(this.repository.findAll()).hasSize(6); >

For each method to be executed, you can provide @SqlGroup and @Sql annotations to execute the scripts before running the actual test.
In this case, we check that the query returns all users in the database, and thanks to the MockMvcResultMatchers.jsonPath , we can check the response of our request and validate that the ids are those expected in ascending order.

@Test @SqlGroup(< @Sql(value = "classpath:empty/reset.sql", executionPhase = BEFORE_TEST_METHOD), @Sql(value = "classpath:init/user-data.sql", executionPhase = BEFORE_TEST_METHOD) >) void should_retrieve_all_users() throws Exception < this.mockMvc.perform(get("/user/fetchAll")) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(5))) .andExpect(jsonPath("$.[0].id").value(1)) .andExpect(jsonPath("$.[1].id").value(2)) .andExpect(jsonPath("$.[2].id").value(3)) .andExpect(jsonPath("$.[3].id").value(4)) .andExpect(jsonPath("$.[4].id").value(5)); >

But we can also add these two annotation on class level, so that we dont have to repeat ourself, constantly.
Here is what our whole class will look like.

@SpringBootTest @AutoConfigureMockMvc @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SqlGroup(< @Sql(value = "classpath:empty/reset.sql", executionPhase = BEFORE_TEST_METHOD), @Sql(value = "classpath:init/user-data.sql", executionPhase = BEFORE_TEST_METHOD) >) class UserControllerTest < @Autowired private UserRepository repository; @Autowired private MockMvc mockMvc; @Test void should_create_one_user() throws Exception < final File jsonFile = new ClassPathResource("init/user.json").getFile(); final String userToCreate = Files.readString(jsonFile.toPath()); this.mockMvc.perform(post("/user/create") .contentType(APPLICATION_JSON) .content(userToCreate)) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath("$").isMap()) .andExpect(jsonPath("$", aMapWithSize(3))); assertThat(this.repository.findAll()).hasSize(6); >@Test void should_retrieve_one_user() throws Exception < this.mockMvc.perform(get("/user/fetch/", 3)) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("$.id").value(3)) .andExpect(jsonPath("$.name").value("Julien Mael")) .andExpect(jsonPath("$.email").value("laura.royer@yahoo.fr")); > @Test void should_retrieve_all_users() throws Exception < this.mockMvc.perform(get("/user/fetchAll")) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)) .andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$", hasSize(5))) .andExpect(jsonPath("$.[0].id").value(1)) .andExpect(jsonPath("$.[1].id").value(2)) .andExpect(jsonPath("$.[2].id").value(3)) .andExpect(jsonPath("$.[3].id").value(4)) .andExpect(jsonPath("$.[4].id").value(5)); >@Test void should_delete_one_user() throws Exception < this.mockMvc.perform(delete("/user/delete/", 2)) .andDo(print()) .andExpect(status().isNoContent()); assertThat(this.repository.findAll()).hasSize(4); > > 

Run the Integration Tests

Now that we have configure our Integration tests for the UserController, we can run them using the maven command.

$ mvn -Dtest=UserControllerTest test 

📬・Thanks for reading

Before jumping into the conclusion, I’m curious to hear if this tutorial helped you. Please let me know your thoughts in the [comments 💬] section below.

You can connect with me on LinkedIn or Twitter.

Also, don’t forget to subscribe to my newsletter below to avoid missing the upcoming posts and the tips and tricks I occasionally share on this blog.

Summary

I’m sure that you have notice that we have not define a real database in this demo project. That’s the power on Integration Tests, we simulate the real world interactions on database without even need to define a real one.

You can find the code source for this demo project on my GitHub.

I don’t recommend you to use H2 InMemory Database for your integration tests but instead Testcontainers, but if you have some limitation (like buying Docker Enterprise license), you can use H2 for that, but keep in mind that even if it’s works, it not the better option.

Источник

Оцените статью