kotest: Kotlintest does not with spring data jpa

@DataJpaTest does not appear to work with kotlintest. The tests fail with the error:

javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call

Note that the standard JUnit 5 test in the same project works.

See sample project for runnable project

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 25 (10 by maintainers)

Commits related to this issue

Most upvoted comments

The test works if you don’t use the Then. The problem is that the nested tests of kotlintest don’t play well with how the spring TransactionalTestExecutionListener works with transactions. The TransactionalTestExecutionListener creates a transaction before a test method is called and closes the transaction after the test method ended.

When you use the default single instance isolation mode something like this happens:

instantiate spec beforeTest for Given create transaction 1 run code in Given beforeTest for When try to create transaction 2 but spring’s TransactionalTestExecutionListener sees that there is already a transaction so throws an error.

With InstancePerLeaf something similar happens.

With InstancePerTest it’s like: instantiate spec beforeTest for Given create transaction 1 run code in Given afterTest for Given close transaction 1 run code in Given before When part beforeTest for When create transaction 2 run code in When afterTest for When close transaction 2 run code in Given before When run code in When before Then Here it fails because there is no transaction created

To me i would more expect to be from Given -> When -> Then as one test. Then for that whole part beforeTest would be called once and 1 transaction would be created. In general i would like it more if it worked like that, at least in the BehaviourSpec but i would say also in the other spec types.

TLDR; if you really want to use kotlintest with spring DataJPATest i would stick to 1 level of tests, so no nested tests. Then you can also use any isolation mode you want probably.

It does work for me, but i need to set IsolationMode to InstancePerTest. If i use SingleInstance or InstancePerLeaf i get:

java.lang.IllegalStateException: Cannot start new transaction without ending existing transaction
	at org.springframework.util.Assert.state(Assert.java:73)
	at org.springframework.test.context.transaction.TransactionalTestExecutionListener.beforeTestMethod(TransactionalTestExecutionListener.java:180)
	at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:289)
	at io.kotlintest.spring.SpringListener.beforeTest(SpringListener.kt:36)
	at io.kotlintest.runner.jvm.TestCaseExecutor.before(TestCaseExecutor.kt:183)
	at io.kotlintest.runner.jvm.TestCaseExecutor.access$before(TestCaseExecutor.kt:35)
	at io.kotlintest.runner.jvm.TestCaseExecutor$execute$3.invokeSuspend(TestCaseExecutor.kt:49)
	at io.kotlintest.runner.jvm.TestCaseExecutor$execute$3.invoke(TestCaseExecutor.kt)

The TransactionalTestExecutionListener expects every test finished before starting a new test, which is not the case for nested tests when using SingleInstance.

Hey @sksamuel here it is. Let me know if you need anything.

It seems like you are using version 3.3.0; you need at least 3.4.2 for this to work.

Ok, this was a very tough one, but I think I managed to do it.

First of all, thanks A LOT for @tunaranch for providing a simple project to test this on. I would have had a hard time to do it myself, as I don’t use this kind of tests a lot.

I’ll add to this thread what I had to do to solve, because I’m very sure we’ll get confused about it in the future.


Solution and explanation

Spring executes transactions (and this include anything inside @DataJPATest) through a Listener called TransactionalTestExecutionListener. This comes pre-registered with a SpringBootTest, and KotlinTest was executing this listener correctly as per #887.

However, only executing the Listener wasn’t enough, as it couldn’t autowire the EntityManager. Initially, this was happening because our SpringListener had a fake “test method” that was a pointer to java.lang.Object.hashcode method. We made this hack to allow us to execute code inside Spring Test Framework, and it seemed fine on our tests.

Due to hashcode being a java.lang.Object method, Spring couldn’t find the transaction attribute, as it verified directly that the method wasn’t from Java Object.

With this in mind, we must use an object from another class. My first attempt was to use a dummy method, declared in the SpringListener itself. But this also wouldn’t work, as the listener would never find the TransactionAttribute, as the method doesn’t (and shouldn’t) declare a @Transaction, and SpringListener also didn’t declare that annotation. The only way to have a method with that annotation would be inside user’s Spec definition, where the user would choose it.

We don’t have methods, as we define everything through a DSL. But that didn’t seem an issue, as Spring’s Listener would get the Transaction Attribute from the method’s class if it doesn’t declare it. @DataJPATest already declare transaction attributes, so if we managed to get a method inside it, it would work without having to declare @Transaction.

If we could guarantee that our users were overriding a method, we could get its reference, but we couldn’t force that, and referecing a method from Spec wouldn’t work (as Spec doesn’t declare the attributes).

The only way to solve this issue that I found was to inject a dummy method into the class, and get it through reflection, so that Spring Listener would correctly find the Transaction Attributes, and that’s what I’ve done.