Coming Up for Air

Testing Spring Repositories with Flyway

With my recent job change, I’ve gotten a chance to use Spring Boot in anger a bit. It’s been fun, and I’ve learned a fair bit about the current state of Spring (I still love you, Jakarta EE!). One of my tasks involved adding a query method to a repository, and I wanted to make sure the query worked before I pushed it upstream. To do that confidently, of course, required a unit test. In this post, I’ll show how remarkably simple it is to test Spring Repositories using Flyway to set up schemas and test data.

To start off the demo, I created a simple Spring Boot project using Spring Intializr.

Opening that in my IDE, I needed to configure the database connection, which I did in src/main/resources/application.properties:

spring.datasource.url=jdbc:postgresql://foo:5432/bar
spring.datasource.username=spring
spring.datasource.password=password
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto = validate

In this case, the production configuration isn’t as interesting as the test config (since this demo doesn’t really do much in "production"). The test config is in src/test/resources/application.properties:`

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto = none

In production, we’re using Postgres, but in test, we’re using H2. Pretty simple.

To set up Flyway, all you really need to do is add it to the classpath:

<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <version>6.0.8</version>
</dependency>

With that dependency in place, when the tests are run (or the application started), Flyway will automatically look for migration scripts on the classpath under db/migration. If you have no scripts, you will see a failure like this:

...
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.flywaydb.core.Flyway]: Factory method 'flyway' threw exception; nested exception is org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingException: Cannot find migration scripts in: [classpath:db/migration] (please add migration scripts or check your Flyway configuration)
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:651) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE]
        ... 99 common frames omitted
Caused by: org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingException: Cannot find migration scripts in: [classpath:db/migration] (please add migration scripts or check your Flyway configuration)
        at org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration$FlywayConfiguration.checkLocationExists(FlywayAutoConfiguration.java:166) ~[spring-boot-autoconfigure-2.2.2.RELEASE.jar:2.2.2.RELEASE]
...

For our demo, we’ll create one migrate, src/main/resources/db/migration/V000__schema.sql:

CREATE TABLE book (
    id number primary key,
    title varchar2(255),
    author varchar2(255),
    description text,
    publish_date date
);

When we run the test now, our schema is automatically created for us in H2, so we’re ready to "test":

@ExtendWith(SpringExtension::class)
@DataJpaTest
class BookRepositoryTest {
    @Autowired
    private lateinit var repository: BookRepository

    @Test
    fun dataWasLoaded() {
        val books = repository.findAll()
        assertThat(books).hasSize(3)

    }
}

Note that we’re running under JUnit 5, so we use @ExtendWith(SpringExtension::class). If you’re using JUnit 4, then you’ll need to use @RunWith(SpringRunner::class). Either way, we annotate the class with @DataJpaTest to tell Spring what we’re testing, and we autowire in our Repository and test as normal.

The first time we run this overly simple test, it will fail, as there’s no data in the database. To fix that, we can use a Flyway migrate to load our data. To make the example a bit more interesting, we’ll add it in a non-standard, as I found myself in that situation. The migrate (src/test/resources/testdata/V999__testdata.sql):

INSERT INTO book (id, title, author, publish_date) VALUES (1, 'The Fellowship of the Ring', 'Tolkien, J.R.R.', '1952-07-29');
INSERT INTO book (id, title, author, publish_date) VALUES (2, 'The Two Towers', 'Tolkien, J.R.R.', '1954-11-11');
INSERT INTO book (id, title, author, publish_date) VALUES (3, 'The Return of the King', 'Tolkien, J.R.R.', '1955-10-20');

If we make no changes, Flyway won’t be able to find the file, so we have to update test/src/resource/application.properties:

...
spring.flyway.locations=classpath:db/migration/,filesystem:../flyway/sql,classpath:testdata/

The value is a comma-delimited (don’t ask me why) of paths to search. Note that we have to prefix each with the type, either classpath or filesystem. In my real world case that drove this line of investigation, our migrates were actually outside of the module’s source directory (as part of a larger multi-module build), so I needed to specifiy a filesystem path, which, it turns out, is relative to the project directory when run like this. We also have to take care to add the default path, classpath:db/migration, or we’ll break thing. Finally, we add our somewhat arbitrary new path, and we’re ready to run our test again, which turn green.

As it turns out, then, testing with Flyway is amazingly simple. Flyway’s also a great way to manage schema changes in your production application. If you’re not using Flyway (or testing your Spring Repositories), I hope this will be the encouragement — and information — you need to get started.

Quotes

Sample quote

Quote source