I've used in-mem databases in Spring JPA tests many times, and never had a problem. This time, I have a bit more complex schema to initialize, and that schema must have a custom name (some of the entities in our domain model are tied to a specific catalog name.) So, for that reason, as well as to ensure that the tests are fully in sync and consistent with the way we initialize and maintain our schemas, I am trying to initialize an in-memory H2 database using Liquibase before my Spring Data JPA repository unit tests are executed.
(Note: we use Spring Boot 2.1.3.RELEASE and MySql as our main database, and H2 is only used for tests.)
I have been following the Spring Reference guide for setting up Liquibase executions on startup. I have the following entries in my Maven POM:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
My test files look like this:
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = PersistenceTestConfig.class)
@DataJpaTest
public class MyRepositoryTest {
@Autowired
private MyRepository myRepository;
@Test
public void someDataAccessTest() {
// myRepository method invocation and asserts here...
// ...
}
}
The app context class:
@EnableJpaRepositories({"com.mycompany.myproject"})
@EntityScan({"com.mycompany.myproject"})
public class PersistenceTestConfig {
public static void main(String... args) {
SpringApplication.run(PersistenceTestConfig.class, args);
}
}
According to the reference guide,
By default, Liquibase autowires the (@Primary) DataSource in your context and uses that for migrations. If you need to use a different DataSource, you can create one and mark its @Bean as @LiquibaseDataSource. If you do so and you want two data sources, remember to create another one and mark it as @Primary. Alternatively, you can use Liquibase’s native DataSource by setting spring.liquibase.[url,user,password] in external properties. Setting either spring.liquibase.url or spring.liquibase.user is sufficient to cause Liquibase to use its own DataSource. If any of the three properties has not be set, the value of its equivalent spring.datasource property will be used.
Obviously, I want my tests to use the same datasource instance as the one Liquibase uses to initialize the database. So, at first, I've tried to specify the spring.datasource properties without providing the spring.liquibase.[url, user, password] properties - assuming that Liquibase would then use the default primary Spring datasource:
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS corp
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=validate
# LIQUIBASE (LiquibaseProperties)
spring.liquibase.change-log=classpath:db.changelog.xml
#spring.liquibase.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS corp
#spring.liquibase.user=sa
#spring.liquibase.password=
spring.liquibase.default-schema=CORP
spring.liquibase.drop-first=true
That didn't work because Liquibase did not find the CORP schema where I must have my tables created:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'liquibase' defined in class path resource [org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguratio n$LiquibaseConfiguration.class]: Invocation of init method failed; nested exception is liquibase.exception.DatabaseException: liquibase.command.CommandExecutionException: liquibase.exception.DatabaseException: liquibase.exception.LockException: liquibase.exception.DatabaseException: Schema "CORP" not found; SQL statement:
CREATE TABLE CORP.DATABASECHANGELOGLOCK (ID INT NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP, LOCKEDBY VARCHAR(255), CONSTRAINT PK_DATABASECHANGELOGLOCK PRIMARY KEY (ID)) [90079-197] [Failed SQL: CREATE TABLE CORP.DATABASECHANGELOGLOCK (ID INT NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP, LOCKEDBY VARCHAR(255), CONSTRAINT PK_DATABASECHANGELOGLOCK PRIMARY KEY (ID))]
So, I took out the explicit spring.datasource property definitions and provided only the following Liquibase properties:
spring.jpa.hibernate.ddl-auto=validate
# LIQUIBASE (LiquibaseProperties)
spring.liquibase.change-log=classpath:db.changelog.xml
spring.liquibase.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS corp
spring.liquibase.user=sa
spring.liquibase.password=
spring.liquibase.default-schema=CORP
spring.liquibase.drop-first=true
That resulted in the Liquibase task executing successfully and seemingly loading all the necessary tables and data into its native datasource at startup - using the provided changelog files. I understand that this happens because I have explicitly set the Liquibase DS properties, and, per Spring documentation, that would cause Liquibase to use its own native datasource. I suppose, for that reason, while the Liquibase job now runs successfully, the tests are still attempting to use a different [Spring default?] datasource, and the database schema fails the pre-test validation. (No "corp" schema found, no tables.) So, it is obvious that the tests use a different datasource instance from the one that I am trying to generate using Liquibase.
How do I make the tests use what Liquibase generates?
Nothing I try seems to work. I suspect that there is some kind of conflict between the auto- and explicit configurations that I am using. Is @DataJpaTest
a good approach in this case. I do want to limit my app context configuration to strictly JPA testing, I don't need anything else for these tests.
It should be simple... However I have not been able to find the correct way, and I can't find any documentation that would clearly explain how to solve this.
Any help is much appreciated!