1

Because I don't have a ton of experience with JPA, I wanted to write an automated test to ensure that my JPA entities are retrieving the records I expect them to retrieve. My plan was to spin up an in-memroy H2 DB for my test, and do a couple simple CRUD operations against it, ensuring that the data is coming back as I expected.

I cannot figure out how to get Spring Boot to create an in-memory database. Here's what I have so far, which isn't working.

This is the configuration that creates my JPA repositories. This is proper application code, and is currently working with my actual Oracle DB.

@Configuration
@EnableJpaRepositories("com.name.project.webservice.dao")
@EntityScan("com.name.project.webservice.types")
public class JpaRepositoriesConfig {
    // Intentionally empty.
}

I import this Config into my test.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {JpaRepositoriesConfig.class})
public class JpaEntityTest {

    @Test
    public void test(){}
}

Then, I write a test-specific properties file,src/test/resources/application.properties, to replace my application's Oracle config with my test's H2 config.

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1

Finally, I add the H2 jar to my pom. The presence of this jar, along with the annotation on my test, should instruct Spring Boot to spin up an H2 database in accordance with my properties file.

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

So now everything should be set up (or so I thought). But when I run my test, the app context fails to start up, giving me this stack trace:

java.lang.IllegalStateException: Failed to load ApplicationContext
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:125)
    at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:108)
    at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190)
    at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132)
    at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:246)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:227)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:246)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'facilityRepository': Cannot create inner bean '(inner bean)#11eadcba' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#11eadcba': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'entityManagerFactory' available
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:361)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:131)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1681)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1433)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:592)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:826)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:742)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:389)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:311)
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:119)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:117)
    ... 25 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#11eadcba': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'entityManagerFactory' available
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:314)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:110)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:662)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:479)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1321)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1160)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:346)
    ... 43 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'entityManagerFactory' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:771)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1221)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:294)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:273)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:303)
    ... 51 more

This error surprised me. I thought Spring Boot was supposed to give me an entityManagerFactory automagically; my app never instantiates an entityManagerFactory bean and it works fine.

So if you wouldn't mind telling me, am I at least on the right track to configuring this correctly? What step did I miss that's causing this error? Do I need to just declare the entityManagerFactory manually?

Thank you for your help.

Edit: Here is the relevant part of my app's properties file, as opposed to my test's properties file recorded above.

spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.url= <<database-url>>
spring.datasource.tomcat.max-active=5
spring.datasource.tomcat.initial-size=1
spring.datasource.tomcat.max-wait=20000 
spring.datasource.tomcat.max-idle=1
Mathew Alden
  • 1,458
  • 2
  • 15
  • 34
  • why you need this class `JpaRepositoriesConfig` and why you kept it empty? can you show how the original config? and also your `application.yml` or properties file – Ryuzaki L Sep 27 '19 at 18:18
  • This is the original config. It's empty because Spring Boot does all the work. Also, I've attached the important part of my application.properties file. – Mathew Alden Sep 27 '19 at 18:26
  • try my answer and i will suggest using profiles – Ryuzaki L Sep 27 '19 at 18:28

2 Answers2

1

I was able to add an in-memory database to the Spring Context by using the following annotations on my Test class:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { JpaRepositoriesConfig.class })
@DataJpaTest

and including the necessary dependency in my pom of course:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

Please let me know if what I'm doing breaks any best practices.

Mathew Alden
  • 1,458
  • 2
  • 15
  • 34
0

I do see you are maintaining separate properties file for testing, which means you are creating datasource and entitymanager with test properties and loading spring boot original application context. so you don't need any additional test configs

@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaEntityTest {

     @Test
     public void test(){}
}

You can also use profiles for doing this, naming application.properties to application-test.properties and use @Profile and @ActiveProfile

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfile("test")
@Profile("test")
public class JpaEntityTest {

     @Test
     public void test(){}
}
Ryuzaki L
  • 37,302
  • 12
  • 68
  • 98
  • 1
    If I remove the "classes" argument, it gives me this error: `java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test` It looks like I need to have that argument, correct? Also, you're right about profiles. I will add them once I get it working. – Mathew Alden Sep 27 '19 at 18:42
  • Does your src classes and test classes are in same package structure? @AugustZellmer – Ryuzaki L Sep 27 '19 at 18:48
  • see this https://stackoverflow.com/questions/39084491/unable-to-find-a-springbootconfiguration-when-doing-a-jpatest @AugustZellmer – Ryuzaki L Sep 27 '19 at 19:00
  • i recommend this approach https://stackoverflow.com/a/56844368/9959152 @AugustZellmer – Ryuzaki L Sep 27 '19 at 19:03
  • The test classes are in the same package as the class they're testing. (But in src/test/java instead of src/main/java of course.) The `@SpringBootApplication` class is in a separate Maven module. – Mathew Alden Sep 27 '19 at 19:03
  • i never tried this approach `The @SpringBootApplication class is in a separate Maven module. –` but try providing main class path like this `@SpringBootTest(classes = Application.class)` – Ryuzaki L Sep 27 '19 at 19:05
  • Right now, I am unable to add `@SpringBootTest(classes = Application.class)`. My test is in module A; Application.class is in module B, and B depends on A so I can't access B from A. I guess my project structure is preventing some of the automatic package scanning from working. I'll have to reconsider how I approach this. – Mathew Alden Sep 27 '19 at 19:10
  • Thanks for your help. I might restructure my project to use your solution, or I might find my own solution. I'll post it when I figure it out. – Mathew Alden Sep 27 '19 at 19:20