I have already reported, years ago, this issue to Mockito here: https://github.com/mockito/mockito/issues/1365
With Spring Boot 1.5.20.RELEASE, I experience a consistent behaviour with class-level Transactional beans, mostly like described in the other issue.
Given a BaseDao<T>
class and its abstract implementation
@Transactional(propagation=MANDATORY)
public class BaseDaoImpl<T> implements BaseDao<T>{
}
I can't @SpyBean
any DAO, which would be useful to me
@RunWith(SpringRunner.class)
@ContextConfiguration
public class SpringBootTest
{
@SpyBean
private MyEntityDao entityDao;
@Test
public void test()
{
doStuff();
ArgumentCaptor<MyEntity> myEntityCaptor = forClass(MyEntity.class);
verify(entityDao).insert(myEntityCaptor.capture());
getErrorCollector().checkThat(myEntity,is(notNullValue()));
}
}
The test epically fails on mock reset with following stack trace:
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:363)
at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:463)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:277)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.boot.test.mock.mockito.MockitoAopProxyTargetInterceptor.invoke(MockitoAopProxyTargetInterceptor.java:66)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy120.getMockitoInterceptor(Unknown Source)
at org.mockito.internal.creation.bytebuddy.SubclassByteBuddyMockMaker.getHandler(SubclassByteBuddyMockMaker.java:133)
at org.mockito.internal.creation.bytebuddy.ByteBuddyMockMaker.getHandler(ByteBuddyMockMaker.java:35)
at org.mockito.internal.util.MockUtil.isMock(MockUtil.java:81)
at org.mockito.internal.util.DefaultMockingDetails.isMock(DefaultMockingDetails.java:32)
at org.springframework.boot.test.mock.mockito.MockReset.get(MockReset.java:109)
at org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener.resetMocks(ResetMocksTestExecutionListener.java:67)
at org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener.resetMocks(ResetMocksTestExecutionListener.java:55)
at org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener.beforeTestMethod(ResetMocksTestExecutionListener.java:45)
at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:269)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.junit.rules.Verifier$1.evaluate(Verifier.java:35)
at org.mockito.internal.junit.VerificationCollectorImpl$1.evaluate(VerificationCollectorImpl.java:36)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
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:191)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
From what I could find, and as described in the other issue, it seems like mandatory transaction is unliked in this case. When Spring Boot resets mocks and spies, it will invoke a getHandler()
method on the spied object. But the spied object, even if it implements the Mockito's callback interface, is still a bean which is globally annotated for mandatory transaction, thus the reset operation requires a transaction to be present.
Conversely, one would argument that we are invoking getMockHandler
and reset
on decoration interfaces, and not on the actual bean. But as ByteBuddy creates a subclass of the actual class to spy, then it's clear that the Spring's transactional semantic kicks in.
Please note that, since mandatory transaction is present at class and not interface level, I can mock the DAO interface with @MockBean. However, I wanted to actually spy the concrete implementation so it had access to real database.
This is preventing me from working on tests, and I am struggling to use workarounds.
Comment From: djechelon
Maybe the Mockito interceptor should be the earliest in the interception chain, and block call to further interceptors if handling the invocation...
Comment From: wilkinsona
@djechelon Apologies for the delay in looking at this one.
Spring Boot 1.5.x reached the end of its supported life in August last year. Things have moved on quite a bit since then. For example, MockitoAopProxyTargetInterceptor
that you can see in the stack trace above no longer exists.
Can you please try your scenario with Spring Boot 2.x (ideally 2.3.x) and see if the problem still occurs? If it does and you would like us to spend some more time investigating, please spend some time providing a complete yet minimal sample that reproduces the problem. You can share it with us by pushing it to a separate repository on GitHub or by zipping it up and attaching it to this issue.
Comment From: djechelon
@wilkinsona I am sorry but I am going to abandon this issue.
Our company is not going to upgrade Spring from the current EOL version. Not my decision.
Comment From: wilkinsona
No problem. Thanks for letting us know. I hope you have some success in the future with getting approval for an upgrade to a supported version.