How to add Integration Tests to our Spring Boot backend

Eduardo Ortega
edataconsulting

--

Spring Data JPA allows us to write queries to the database in several ways. The simplest option is to use inferred queries, where Spring creates the query from the repository’s method name, but there are occasions where we need more complex queries and we must write them directly using JPQL or native SQL.

Since the possibility of making mistakes increases when using such queries, it is necessary to test them, which leads to the need to add integration tests to our project.

So, let’s check step by step how to do that using Maven.

1. Separate Unit and Integration Tests

I prefer to separate tests using JUnit 5 tags rather than using regular patterns to search for test files by name. So, I create two Java annotations to encapsulate the JUnit tags. (This is not strictly necessary, but makes renaming the tags easier if needed, and you can also add other annotations to all the tests which are grouped at once. There is an example of this in section 4.)

For our example, the annotations can be declared as in the first code block, and we use them as in the second.

With this done, we can create different configurations in our IDE to run the tests separately and in this way speed up the development as we won’t need to wait for the integration tests to run every time we want to run the unit tests:

Image 1: Configuration to filter tests in IntelliJ IDEA.

2. How to write the Integration Tests

As an example, the integration test for our repository could be as shown below. Here I’m not using a custom query, but it serves well to check that the tests are being executed. To test a repository, we need to use DataJpaTest annotation (included in spring-boot-starter-test artifact).This will disable full-auto-configuration and instead configure only those components relevant for JPA tests (e.g. Repository and Entity). This annotation also makes all our tests transactional, which means the transaction will roll back after each test is executed by default.

The Sql annotation is used to set up the database state before the tests. We can use it at class level (which executes the statements before each test) or at method level (which overrides the class level if used). The annotation allows us to execute statements directly (as shown in the example) or load them from a file.

3. Configure the tests in Maven

The phases of Maven’s Build Lifecycle that are closely related to the tests are the following:

  • test: run tests using a unit testing framework. These tests should not require the code to be packaged or deployed.
  • pre-integration-test: perform actions required before integration tests are executed. This may involve tasks such as setting up the required environment.
  • integration-test: if necessary, process and deploy the package into an environment where integration tests can be run.
  • post-integration-test: perform actions required after integration tests have been executed. This may include cleaning up the environment.
  • verify: run any checks to verify the package is valid and meets quality criteria.

From the previous list, only the test and verify phases can be executed from the command line. The phases related to integration tests will be triggered before the verification phase. As can be read in Maven’s documentation:

The phases named with hyphenated-words (pre-*, post-*, or process-*) are not usually directly called from the command line. These phases sequence the build, producing intermediate results that are not useful outside the build. In the case of invoking integration-test, the environment may be left in a hanging state.

To execute our tests, we need to add two different plugins to our pom.xml (notice in the code block below that I’m using spring-boot-starter-parent so I don’t need to specify plugin versions):

  • maven-surefire-plugin: this will run all the tests tagged as “UnitTest” (line 18).
  • maven-failsafe-plugin: this will run all the tests tagged as “IntegrationTest” (line 25).

Now we need to start the spring-boot app to run the integration tests. To do so, we will add Spring Boot Maven Plugin to our pom.xml as shown below. Keep in mind that the number of milliseconds in the wait property may vary from one application to another. By default, the maxAttempts property is set to 60, so in this example, the global timeout for the application to start is 90 seconds (1500 * 60 = 90000 milliseconds). The jmxPort property specifies the port in which the app will be available. This is not required unless the default port is taken.

Also, notice lines 21 to 29: there are two ways to tell Spring what the active profile for running the integration tests is. This is important as we may need some properties to startup the application.

4. Create application.properties file for integration-test profile

Now that we have Maven configured, we just need to create a new properties file (application-integration-test.properties) and override the required properties to startup the application.

As we are going to override our datasource properties, we should modify our IntegrationTest annotation as below to ensure all classes annotated with it are executed with the right profile and datasource:

The first thing to consider is whether we want to use an in-memory database or a Testcontainer using Docker. If we are using some kind of database migration tool such as Flyway, we should keep in mind that there is a drawback when using an in-memory database for our tests: the written SQL has to work in both (production and integration-test) databases. If our migrations use some database-specific syntax, it might not work with the H2 database. I will describe both cases.

Case 1: In-memory database

In this case, we will disable Flyway and use the H2 database, so we need to add the corresponding dependency to our pom.xml and set the scope to runtime so that Spring can load the driver properly.

Notice that in this example:

  • Hibernate will use our entities to create the database schema.
  • Property DB_CLOSE_DELAY=-1 is necessary to prevent the schema from being deleted when the statements finish executing.
  • Property DATABASE_TO_UPPER=false is necessary to prevent name errors when accessing fields.

Case 2: Testcontainer database

In this case, we will enable Flyway and use a Testcontainer, so we need to add the corresponding dependency to our pom.xml and set the scope to runtime so that Spring can load the driver properly.

Notice that in this example:

  • Hibernate will validate the database schema that was created by Flyway.
  • This approach will take longer to run, since a docker container must be started.

5. How to run the tests

To run our tests, we can execute any of these commands: mvn verify, mvn install, or mvn deploy.

It is important to note that we don’t need to run these commands using the integration-test profile as we are changing the active profile using spring-boot-maven-plugin.

Conclusion

Dealing with integration tests can be tricky depending on how your project is built and configured, but it is important to add them as a good test harness helps to improve the quality of the project.

I hope this article will be able to help you as a starting point. Thanks for reading! 😄

--

--