Cleaning up database tables after each integration test method with Spring Boot 2 and Kotlin

I usually prefer to write integration tests that perform end-to-end processing (e.g., flow from a client making a REST POST/PUT or GraphQL mutation up to the database access layer and back out). I’m not a big fan of mocking parts of the system for integration tests.

Of course “integration tests” mean different things to different people but I won’t get into that.

One issue that I’ve faced when writing my integration tests with Spring Boot 2 (that I’m still discovering) is that it wasn’t very straightforward to clean up the in-memory database after each test method/class.

It was pretty easy when I wrote tests using @DataJpaTest, since I only needed to add @Transactional, but @DataJpaTest only loads specific parts of the application in order to test the data access layer.

What I wanted and what I’m using now is @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT), which does what I want: starts the closest thing to the real application. Actually this feels like what I previously obtained (a lot more painfully) using Arquillian.

With @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT), you must forget about @Transactional in test classes. This is clearly stated in Spring Boot’s documentation:

If your test is @Transactional, it will rollback the transaction at the end of each test method by default. However, as using this arrangement with either RANDOM_PORT or DEFINED_PORT implicitly provides a real servlet environment, HTTP client and server will run in separate threads, thus separate transactions. Any transaction initiated on the server won’t rollback in this case.

Thus indeed, there’s no easy way to rollback the changes...

The simplest thing that could work would be to perform the cleanup manually at the end of each test method/class, but that is really cumbersome and error-prone. What I want instead is something easy that does the cleanup for me.

Another option could be to add DbUnit and Spring DbUnit to my project and use @DatabaseSetup (cfr example project) which uses DatabaseOperation.CLEAN_INSERT.

But for now I don’t want to add DbUnit to my project.

Another option that I’ll look at later on is this one. I like the approach but I’m using JUnit 5 and I don’t have time to go about and learn what JUnit rules have become. Although, since that’s also interesting, I’ll come back to it in a future article =)

So here’s what I’ve ended up doing this evening. It’s the very first iteration, so indeed there are tons of things to improve there! ;-)

First I’ve defined a “test” profile that I activate in my tests using:

@ActiveProfiles("test")

Then I took (a lot of) inspiration from the following article and adapted the code to avoid depending on Hibernate:

...

import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import javax.persistence.EntityManager
import javax.persistence.Table
import javax.persistence.metamodel.Metamodel
import kotlin.reflect.full.findAnnotation

/**
* Test utility service that allows to truncate all tables in the test database.
* Inspired by: http://www.greggbolinger.com/truncate-all-tables-in-spring-boot-jpa-app/
*
@author Sebastien Dubois
*/
@Service
@Profile("test")
class DatabaseCleanupService @Autowired constructor(private val entityManager: EntityManager) : InitializingBean {
private lateinit var tableNames: List<String>

/**
* Uses the JPA metamodel to find all managed types then try to get the [Table] annotation's from each (if present) to discover the table name.
* If the [Table] annotation is not defined then we skip that entity (oops :p)
*/
override fun afterPropertiesSet() {
val metaModel: Metamodel = entityManager.metamodel
tableNames = metaModel.managedTypes
.filter {
it
.javaType.kotlin.findAnnotation<Table>() != null
}
.map {
val tableAnnotation: Table? = it.javaType.kotlin.findAnnotation()
tableAnnotation?.name ?: throw IllegalStateException("should never get here")
}
}

/**
* Utility method that truncates all identified tables
*/
@Transactional
fun truncate() {
entityManager.flush()
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate()
tableNames.forEach { tableName ->
entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate()
}
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate()
}
}

As you can see it’s a simple Spring service that uses the JPA metamodel to go about and find the table names through reflection (by getting the name from the @Table annotation on the entities).

With that in place, I’ve created a base class for my integration tests where I inject and use the DatabaseCleanupService after each test method:

...

import mu.KotlinLogging
import org.junit.jupiter.api.AfterEach
import org.springframework.beans.factory.annotation.Autowired

private val logger = KotlinLogging.logger {} // static and uses the unit name

/**
* Makes sure that the test DB is cleaned up after each test
*
@author Sebastien Dubois
*/
open class AbstractIntegrationTest : AbstractTest() {
@field:Autowired
private lateinit
var truncateDatabaseService: DatabaseCleanupService

// TODO add @BeforeEach, BeforeClass, After... and a parameter (enum) to define if and when to perform the cleanup

/**
* Cleans up the test database after each test method.
*/
@AfterEach
fun cleanupAfterEach() {
logger.info { "Cleanup up the test database" }
truncateDatabaseService.truncate()
}
}

Using the above is pretty simple and fool proof: inherit from that class and the database will be cleaned after each test method has completed.

Of course this is far from perfect and will probably break for more complex models/scenarios, but it fits my current (i.e., basic) needs.

What I plan to improve now is to avoid the is-a relationship and providing more flexibility regarding when/what to clean up. This should be pretty straightforward to implement now :)

That’s it for today!

Written by

Author, CTO. Subscribe to my newsletter: https://mailchi.mp/fb661753d54a/developassion-newsletter. Follow me on Twitter https://twitter.com/dsebastien

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store