Overview
In a Spring Boot project, with a test such as this:
@DataJpaTest
class PersonRepositoryTest {
@Autowired
PersonRepository personRepository;
@Autowired
TestEntityManager em;
@BeforeEach
void setup() {
em.persistAndFlush(new Person());
}
@Test
void shouldFindAllPersons() {
Iterable<Person> result = personRepository.findAll();
assertThat(result).hasSizeGreaterThanOrEqualTo(1);
}
@Nested
@TestPropertySource(properties = "spring.config.additional-location=optional:file:data.yaml")
class FindAll {
@Test
void shouldFindAllPersons() {
Iterable<Person> result = personRepository.findAll();
assertThat(result).hasSizeGreaterThanOrEqualTo(1);
}
}
}
The test in the @Nested class fails with:
jakarta.persistence.TransactionRequiredException: no transaction is in progress
The test in the outer class works fine.
Removing the @TestPropertySource results in all tests passing, however in the test I'm actually working on, I need that to work around another bug. I assume that there are other scenarios which might cause this issue as well. I've spent some time debugging this, and it appears that 2 LocalContainerEntityManagerFactoryBeans are created - the transaction is started with one of them, but the code in the @BeforeEach is executed using the other. This results in the TransactionSynchronisationManager not finding the EntityManagerHolder in doGetResource().
The issue is present in 3.2.3, 3.2.12 and 3.4.3, and presumably everything in-between.
The root cause of the issue appears to be that an application context is created for the outer class, and the TestEntityManager is injected from that one, but then another application context is created for the inner class (which makes sense, since the configuration is different), and the transaction is started with the entity manager from the inner class' application context. However, the TestEntityManager has been injected from the outer class' application context, and any operations done using it do not have a transaction in progress.
I initially raised this issue against Spring Boot (https://github.com/spring-projects/spring-boot/issues/44679), but after more investigation, it appears to be an issue with spring-test (present in version 6.2.3).
Related Issues
-
28466
Comment From: sbrannen
Hi @bernie-schelberg-invicara,
Thanks for reporting the issue.
Please note that this is somewhat related to #28466, and the behavior you have experienced is to be expected.
Although @Nested test classes can be quite useful, for certain scenarios with application contexts managed by the Spring TestContext Framework, you can run into issues like the one you have encountered.
We can convert your Spring Data JPA slice test to a standard Spring Framework test as follows. For the sake of brevity, I have omitted the corresponding @Configuration class.
@SpringJUnitConfig
@Transactional
class PersonRepositoryTests {
@Autowired
PersonRepository personRepository;
@Autowired
EntityManager em;
@BeforeEach
void setup() {
em.persist(new Person());
em.flush();
}
@Test
void shouldFindAllPersons() {
List<Person> result = personRepository.findAll();
assertThat(result).hasSizeGreaterThanOrEqualTo(1);
}
@Nested
@TestPropertySource(properties = "nested = true")
class FindAll {
@Test
void shouldFindAllPersons() {
Iterable<Person> result = personRepository.findAll();
assertThat(result).hasSizeGreaterThanOrEqualTo(1);
}
}
}
The above fails the same as your example using Spring Boot and @DataJpaTest.
However, if we modify the test classes so that dependencies are injected via @Autowired method arguments instead of instance fields as follows...
@SpringJUnitConfig
@Transactional
class PersonRepositoryTests {
@BeforeEach
void setup(@Autowired EntityManager em) {
em.persist(new Person());
em.flush();
}
@Test
void shouldFindAllPersons(@Autowired PersonRepository personRepository) {
List<Person> result = personRepository.findAll();
assertThat(result).hasSizeGreaterThanOrEqualTo(1);
}
@Nested
@TestPropertySource(properties = "nested = true")
class FindAll {
@Test
void shouldFindAllPersons(@Autowired PersonRepository personRepository) {
Iterable<Person> result = personRepository.findAll();
assertThat(result).hasSizeGreaterThanOrEqualTo(1);
}
}
}
Then the tests pass as you would expect.
The reason is that JUnit Jupiter invokes the SpringExtension as a ParameterResolver for each method, supplying the ExtensionContext for the currently (or about to be) executing test method.
The tricky part is that, when JUnit runs the shouldFindAllPersons() test in FindAll, it will first invoke setup() on the enclosing PersonRepositoryTests instance with an ExtensionContext that correlates to the ApplicationContext loaded for FindAll (not the ApplicationContext loaded for PersonRepositoryTests).
Please give that a try with your project and let me know if that works for you.
In the interim, I am closing this issue.
Comment From: bernie-schelberg-invicara
@sbrannen Thanks for following up on this. @SpringJUnitConfig didn't work for me, since the project is a Spring Boot project and relies on auto configuration to instantiate the testcontainer-based datasource, and I don't have an @Configuration class which worked out of the box. Using @SpringBootTest instead is a bit of a pain because I'd need to also set up test Kafka and Redis containers, amongst other things. I might be able to get something working if I mess around with it some more but I'm not sure the effort required would be worth it. For now I've flattened my tests and avoided the use of @Nested.
Comment From: sbrannen
Hi @bernie-schelberg-invicara,
Thanks for following up on this.
You're welcome!
@SpringJUnitConfigdidn't work for me, since the project is a Spring Boot project and relies on auto configuration to instantiate the testcontainer-based datasource, and I don't have an@Configurationclass which worked out of the box. Using@SpringBootTestinstead is a bit of a pain because I'd need to also set up test Kafka and Redis containers, amongst other things.
I apologize for not being clear. I didn't mean that you should use @SpringJUnitConfig in your own project. I only used that to reproduce the issue without Spring Boot in the picture.
You should be able to continue using @DataJpaTest.
I might be able to get something working if I mess around with it some more but I'm not sure the effort required would be worth it. For now I've flattened my tests and avoided the use of
@Nested.
The only changes you have to make to get it to work are:
- Stop using
@Autowiredon fields. - Start using
@Autowiredon parameters in@BeforeEachand@Testmethods.
@BeforeEach
void setup(@Autowired EntityManager em) {
// ...
}
@Test
void shouldFindAllPersons(@Autowired PersonRepository personRepository) {
// ...
}
// ...
Comment From: vpavic
@sbrannen since method injection of test dependencies seems to be more flexible and robust (vs old-school field injection), and also makes test code more readable and concise, would you consider promoting it more in the documentation? I just went through the entire testing chapter and field injection is by far the more dominant pattern in the provided examples.
Comment From: sbrannen
Please note that this issue has effectively been superseded by:
-
35676
-
35680
Comment From: sbrannen
Hi @vpavic,
since method injection of test dependencies seems to be more flexible and robust (vs old-school field injection), and also makes test code more readable and concise, would you consider promoting it more in the documentation? I just went through the entire testing chapter and field injection is by far the more dominant pattern in the provided examples.
Indeed, there is room for improvement in the documentation.
And in light of the two issues I referenced in my previous comment (plus some last-minute plans I have for 7.0), there's quite a bit piling up that needs to be documented. 😉