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. Furthermore, 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
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.
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 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String email;
}
Dependencies
To be able to use some methods in this demo project, you have to use all these additional dependencies.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.7.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
</dependencies>
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<User> 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 wondered how to test the Service part of our system, I wrote an article 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/{id}")
@ResponseStatus(HttpStatus.OK)
public User retrieveUser(@PathVariable int id) {
return this.service.findOneUser(id);
}
@GetMapping("/fetchAll")
@ResponseStatus(HttpStatus.OK)
public List<User> retrieveUsers() {
return this.service.findAllUser();
}
@DeleteMapping("delete/{id}")
@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 data 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
{
"id": 6,
"name": "Arsene Kevin",
"email": "akevin@yahoo.fr"
}
Integration Test
Now you have to define your Integration Class
@SpringBootTest
@AutoConfigureMockMvc
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class UserControllerTest {
@Autowired
private UserRepository repository;
@Autowired
private MockMvc mockMvc;
}
- 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
in our test class to enable and configure autoconfiguration of MockMvc. - 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 In Memory 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);
}
@Sql
is used to run our SQL queries located in the classpath.
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 annotations on class level so that we don't have to repeat ourselves, 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/{id}", 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/{id}", 2))
.andDo(print())
.andExpect(status().isNoContent());
assertThat(this.repository.findAll()).hasSize(4);
}
}
Run the Integration Tests
Now that we have configured our Integration tests for the UserController, we can run them using the maven command.
$ mvn -Dtest=UserControllerTest test
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts or opinions below in the comments section.
You can have more of my content by following me on LinkedIn • GitHub or Medium
Summary
I’m convinced that you have noticed that we have not defined a real database in this demo project. That’s the power of 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 In Memory Database for your integration tests but instead Testcontainers. If you have some limitation (like buying a Docker Enterprise license), you can use H2 for that, but keep in mind that even if it works, it not the better option.