Spring-boot: @MockBean doesn't work with @Async annotated services

Created on 17 Jul 2016  Â·  23Comments  Â·  Source: spring-projects/spring-boot

Hello everybody,

I want to test a MVC @Controller with 1.4.0.RC1 using @WebMvcTest. The controller depends on a service which as an @Async method:

@RunWith(SpringRunner.class)
@WebMvcTest(TestController.class)
public class TestControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private TestService testService;

    @Test
    public void testSomeMethod() {
        // blah...
    }
}

The service:

package com.example;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class TestService {

    @Async
        public int doSomethingElse() {
            return 4711;
        }
}

Test fails with

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'testController' defined in file [/Users/msimons/Downloads/mock_async_proxy_bug/target/classes/com/example/TestController.class]: Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.example.TestController]: Illegal arguments for constructor; nested exception is java.lang.IllegalArgumentException: argument type mismatch
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:279) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1143) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1046) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:775) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:861) ~[spring-context-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541) ~[spring-context-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759) ~[spring-boot-1.4.0.RC1.jar:1.4.0.RC1]
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:369) ~[spring-boot-1.4.0.RC1.jar:1.4.0.RC1]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:313) ~[spring-boot-1.4.0.RC1.jar:1.4.0.RC1]
    at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:111) ~[spring-boot-test-1.4.0.RC1.jar:1.4.0.RC1]
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98) ~[spring-test-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116) ~[spring-test-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    ... 25 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.example.TestController]: Illegal arguments for constructor; nested exception is java.lang.IllegalArgumentException: argument type mismatch
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:156) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:122) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:271) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    ... 42 common frames omitted
Caused by: java.lang.IllegalArgumentException: argument type mismatch
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_92]
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_92]
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_92]
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_92]
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:147) ~[spring-beans-4.3.1.RELEASE.jar:4.3.1.RELEASE]
    ... 44 common frames omitted

My first guess that this is related to #5837 or similar. Throwing @EnableAsync at the test didn't help either, using a separate context configuration would defeat the purpose of WebMvcTest i guess

Example project is attached mock_async_proxy_bug.tar.gz
Actual use case was writing a test for this controller in preparation for 1.4.

Thanks,
Michael.

Most helpful comment

It looks like the @Async support has created the wrong sort of proxy. It's trying to inject a JDK proxy which has been created because the mocked bean implements a single interface, org.mockito.cglib.proxy.Factory.

A workaround is to force the creation of CGLib proxies:

@EnableAsync(proxyTargetClass=true)

All 23 comments

It looks like the @Async support has created the wrong sort of proxy. It's trying to inject a JDK proxy which has been created because the mocked bean implements a single interface, org.mockito.cglib.proxy.Factory.

A workaround is to force the creation of CGLib proxies:

@EnableAsync(proxyTargetClass=true)

The same problem occurs if the mocked bean is created manually so @MockBean isn't making things worse than they normally would be.

So that means I basically have two options: Change the behavior of the application by changing the annotation on the application class or adding an additional configuration with "test" profile or something like that? Just adding @EnableAsync(proxyTargetClass=true) won't do…

I understand what's happening and the workaround would work for me I guess but I guess that behavior would come as a surprise to many.

Hm… I tried to implement the workaround in my project and not only in the demo: @EnableCaching certainly causes the same error…

Well, that's exactly the same root cause...

but I guess that behavior would come as a surprise to many.

Agreed. We need to do something better here, I'm just not sure what yet.

Change the behavior of the application by changing the annotation on the application class

This shouldn't be (much of) a change in behaviour. You'll end up with a different type of proxy (CGLib rather than JDK) but that shouldn't matter to your application.

or adding an additional configuration with "test" profile or something like that?

That may work, although I'm not sure which @EnableAsyc annotation would win.

Just adding @EnableAsync(proxyTargetClass=true) won't do…

Not sure I've understood this. You already have @EnableAsync. A workaround is to add proxyTargetClass=true to the existing annotation.

@EnableCaching certainly causes the same error…

Caching uses proxies too and, in its default configuration, it uses JDK proxies. It has a proxyTargetClasses attribute too.

Agreed. We need to do something better here, I'm just not sure what yet.

Thanks, Andy! As well as I understand

This shouldn't be (much of) a change in behaviour. You'll end up with a different type of proxy (CGLib rather than JDK) but that shouldn't matter to your application.

it will probably raise some questions…
I wish I could help more but that proxy area is not my usual territory.

That may work, although I'm not sure which @EnableAsyc annotation would win.

If I have @Enable… at application class level, it win's and I cannot overwrite it through separate test configuration.

Not sure I've understood this. You already have @EnableAsync. A workaround is to add proxyTargetClass=true to the existing annotation.

I was hoping I could overwrite without introducing new configuration classes by adding it to the test class itself.

I was hoping I could overwrite without introducing new configuration classes by adding it to the test class itself.

I wouldn't recommend doing that. It doesn't feel like a good idea to be running your tests with different configuration from normal runtime.

It feels like the best that we can do here is to make it more obvious that you need to configure proxyTargetClass=true. I've opened SPR-14478 to see what can be done at the Spring Framework level.

I've generally found proxyTargetClass=true to be a sensible default for most applications. The single interface problem is really common and quite hard to debug. Since CGLib is shipped as part of Spring Framework these days, there aren't many downsides.

@philwebb What does that mean:

This approach has no negative impact in practice unless one is explicitly expecting one type of proxy vs. another — for example, in tests.

From the @EnableAsync doc…

Anyway, why don't I get CGLIB proxies anyway? The service in the example doesn't implement any interface…

@michael-simons The service doesn't directly implement an interface, but when mock(...) is in mix Mockito is adding one. Here's what we think happens:

  • @MockBean on TestService causes a mockito mock to be created for TestService.
  • Mockto uses its own packaged version of CGLib. This actually creates a class that extends the real TestService (overriding methods to support mocking) and _also_ implements a org.mockito.cglib.proxy.Factory interface.
  • Spring sees the @Async annotation and decided to create a proxy to support it.
  • Since TestService now implements Factory, Spring decided to use a JDK proxy.
  • You now have a bean that's an instance of Factory but isn't a TestService.

@philwebb my take on this would be to copy/paste your last comment (with some polishing) in the doc.

I disagree, for now at least. Users shouldn't have to know any of that. The ideal is that it just works. I doubt we'll get there in Boot 1.4 and Framework 4.3, but there's still plenty of scope for improvement. I'd like to wait and see what SPR-14478 brings us.

Thanks everybody, for the explanation and workaround and also for the last comment, that is exactly which I would expect as a user, @wilkinsona.

With the latest Spring Framework 4.3.2 snapshot, the provided example passes even with proxyTargetClass=false. The Mockito interface is now ignored and a CGLib proxy is created.

@wilkinsona good news! But you mean false, or? true was the workaround …

@michael-simons Sorry. I meant false. I've updated my comment.

If I update the example so that TestService implements an interface but it's still injected as TestService then we now get some more helpful diagnostics:

Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'com.example.TestService#0' is expected to be of type [com.example.TestService] but was actually of type [com.sun.proxy.$Proxy73]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:378) ~[spring-beans-4.3.2.BUILD-SNAPSHOT.jar:4.3.2.BUILD-SNAPSHOT]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-4.3.2.BUILD-SNAPSHOT.jar:4.3.2.BUILD-SNAPSHOT]
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:207) ~[spring-beans-4.3.2.BUILD-SNAPSHOT.jar:4.3.2.BUILD-SNAPSHOT]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1213) ~[spring-beans-4.3.2.BUILD-SNAPSHOT.jar:4.3.2.BUILD-SNAPSHOT]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1053) ~[spring-beans-4.3.2.BUILD-SNAPSHOT.jar:4.3.2.BUILD-SNAPSHOT]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1018) ~[spring-beans-4.3.2.BUILD-SNAPSHOT.jar:4.3.2.BUILD-SNAPSHOT]
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835) ~[spring-beans-4.3.2.BUILD-SNAPSHOT.jar:4.3.2.BUILD-SNAPSHOT]
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741) ~[spring-beans-4.3.2.BUILD-SNAPSHOT.jar:4.3.2.BUILD-SNAPSHOT]
    ... 42 common frames omitted

I think we should add a FailureAnalyzer for BeanNotOfRequiredTypeException that recommends proxyTargetClass=true when the actual type is a com.sun.proxy.$Proxy

I've opened #6434 for the FailureAnalyzer and we have #6318 for the Spring Framework 4.3.2 upgrade which fixes this issue's specific problem.

Hello,

I experience issues which seem to be closely related to this thread in Spring Boot 1.4.0.RELEASE - should I go on or should I open a new issue for this?

Thanks
Gregor

@WalternativE Thanks for asking. Please open a new issue, ideally with a small sample that reproduces the problem

Hi @WalternativE Would you please link this? I think just stumbled onto something as well and I wanna check if it's the same.

I ran into the same issue. after add "proxyTargetClass = true" it works.
it's a bug or not?
thanks

Was this page helpful?
0 / 5 - 0 ratings