Skip to content

Mockito cannot mock AspectJ-woven @Async method #24724

Closed as duplicate
Closed as duplicate

Description

@cdalexndr

When trying to mock an @Async method from a @SpyBean I get the following error:

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Misplaced or misused argument matcher detected here:

-> at package.TestClass.init_aroundBody0(File.java:79)
-> at package.TestClass.init_aroundBody0(File.java:79)

You cannot use argument matchers outside of verification or stubbing.
Examples of correct usage of argument matchers:
    when(mock.get(anyInt())).thenReturn(null);
    doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject());
    verify(mock).someMethod(contains("foo"))

This message may appear after an NullPointerException if the last matcher is returning an object 
like any() but the stubbed method signature expect a primitive argument, in this case,
use primitive alternatives.
    when(mock.get(any())); // bad use, will raise NPE
    when(mock.get(anyInt())); // correct usage use

Also, this error might show up because you use argument matchers with methods that cannot be mocked.
Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
Mocking methods declared on non-public parent classes is not supported.

This error only appears when I run the test without debugger attached. When I try to debug, the error doesn't show up, and everything works fine.
When I remove the @Async, then it works also without the debugger attached.

This error started showing up after I upgraded from spring 4 to spring 5.
Using spring-boot-starter-test 2.1.6.RELEASE (mockito-core:2.23.4, spring-test:5.1.8.RELEASE)

Activity

  1. sbrannen commented on Jul 20, 2019

    @sbrannen
    Member

    Due to the involvement of @SpyBean, it sounds to me like it might be an issue in Spring Boot Test.

    @philwebb and @wilkinsona, have you come across something like this before?

  2. philwebb commented on Jul 20, 2019

    @philwebb
    Member

    We've seen a few issue like this in the past related to AOP. I'll transfer the issue.

  3. philwebb commented on Jul 20, 2019

    @philwebb
    Member

    @cdalexndr Could you please provide a sample that shows the problem. Preferably something we can just download or clone and run.

  4. spring-projects-issues commented on Jul 27, 2019

    @spring-projects-issues
    Collaborator

    If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

  5. cdalexndr commented on Jul 27, 2019

    @cdalexndr
    Author

    Test project at: https://github.com/cdalexndr/spring-boot-issue-17593
    Just run gradlew test

    After some playing with various settings, I found three ways to cause the tests to pass and I've commented them in the code as:

    //commenting this line causes test to pass (1 of 3)
    //commenting this line causes test to pass (2 of 3)
    //commenting this line causes test to pass (3 of 3)
    
  6. mbhave commented on Aug 8, 2019

    @mbhave
    Contributor

    Thanks for the sample @cdalexndr.

    @philwebb @sbrannen This doesn't seem to be Spring Boot specific as using regular mocks and removing @SpringBootTest seems to cause the same behavior. Changing AdviceMode from ASPECTJ to PROXY gets rid of the failure so it doesn't look like it's related to spring-projects/spring-boot#6573.

  7. wilkinsona commented on Aug 27, 2019

    @wilkinsona
    Member

    @mbhave I've tried and failed to reproduce the problem without @SpyBean. Do you still have the code that you used anywhere?

  8. mbhave commented on Aug 28, 2019

    @mbhave
    Contributor

    Sorry, @wilkinsona, I should've copied what I had in the comment above. I used the sample provided above and this is what I'd changed the test to:

    @ContextConfiguration(classes = Application.class)
    public class Test extends AbstractTestNGSpringContextTests {
        private static final Logger log = LoggerFactory.getLogger( Test.class );
    
        @Spy
        Service service;
    
        @BeforeMethod
        public void setUp() {
    	MockitoAnnotations.initMocks(this);
        }
    
        @org.testng.annotations.Test
        public void test() {
            assertTrue( MockUtil.isSpy( service ) );
            doReturn( new CompletableFuture<>() ).when( service ).asyncMethod( any( Integer.class ) );
            doReturn( null ).when( service ).work(); //commenting this line causes test to pass (3 of 3)
            CompletableFuture<Integer> future = service.asyncMethod( 1 );
            log.info( "Success" );
        }
    }
  9. self-assigned this
    on Mar 12, 2020
  10. wilkinsona commented on Mar 18, 2020

    @wilkinsona
    Member

    The following reproduces the problem:

    package example;
    
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.Mockito.doReturn;
    
    import java.util.concurrent.CompletableFuture;
    
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.MockitoAnnotations;
    import org.mockito.Spy;
    import org.mockito.internal.util.MockUtil;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    import org.springframework.context.annotation.AdviceMode;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.stereotype.Service;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @ContextConfiguration(classes = Application.class)
    @RunWith(SpringRunner.class)
    public class AsyncSpyTests {
    
    	private static final Logger log = LoggerFactory.getLogger(AsyncSpyTests.class);
    
    	@Spy
    	ExampleService service;
    
    	@Before
    	public void setUp() {
    		MockitoAnnotations.initMocks(this);
    	}
    
    	@Test
    	public void test() {
    		assertThat(MockUtil.isSpy(this.service)).isTrue();
    		doReturn(new CompletableFuture<>()).when(this.service).asyncMethod(any(Integer.class ));
    		doReturn(null).when(this.service).work(); // commenting this line causes test to pass
    		this.service.asyncMethod(1);
    		log.info("Success");
    	}
    
    }
    
    @Service
    class ExampleService {
    	
    	private static final Logger log = LoggerFactory.getLogger( ExampleService.class );
    
    	@Async // commenting this line causes test to pass
    	public CompletableFuture<Integer> asyncMethod(Integer param) {
    		work();
    		return new CompletableFuture<>();
    	}
    
    	protected Integer work() {
    		log.info("Work");
    		return 0;
    	}
    	
    }
    
    @EnableAutoConfiguration // commenting this line causes test to pass
    @ComponentScan
    @Configuration
    @EnableAspectJAutoProxy
    @EnableAsync(mode = AdviceMode.ASPECTJ)
    class Application {
    
    }
    plugins {
        id 'org.springframework.boot' version '2.2.5.RELEASE'
    }
    
    apply plugin: 'java'
    apply plugin: 'io.spring.dependency-management'
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation group: 'org.aspectj', name: 'aspectjweaver'
        implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop'
        implementation 'org.springframework:spring-aspects'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    
    test {
        File springAgent = configurations.testRuntimeClasspath.filter { f -> f.name.matches("aspectjweaver.*\\.jar") }.iterator().next();
        jvmArgs += ["-javaagent:" + springAgent.absolutePath]
    }

    Note that AsyncSpyTests is itself not using any Spring Boot functionality but Application is due to its use of @EnableAutoConfiguration. The test passes when this line is commented out so it appears that there is something that Spring Boot is doing that's causing the problem.

  11. wilkinsona commented on Mar 18, 2020

    @wilkinsona
    Member

    Here's a modified version of the code above that reproduces the problem without any involvement from Spring Boot. The key difference is that @EnableAutoConfiguration has been replaced by a TaskExecutor @Bean on the Application class.

    package example;
    
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.Mockito.doReturn;
    
    import java.util.concurrent.CompletableFuture;
    
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.MockitoAnnotations;
    import org.mockito.Spy;
    import org.mockito.internal.util.MockUtil;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.annotation.AdviceMode;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    import org.springframework.core.task.TaskExecutor;
    import org.springframework.scheduling.annotation.Async;
    import org.springframework.scheduling.annotation.EnableAsync;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    import org.springframework.stereotype.Service;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @ContextConfiguration(classes = Application.class)
    @RunWith(SpringRunner.class)
    public class AsyncSpyTests {
    
    	private static final Logger log = LoggerFactory.getLogger(AsyncSpyTests.class);
    
    	@Spy
    	ExampleService service;
    
    	@Before
    	public void setUp() {
    		MockitoAnnotations.initMocks(this);
    	}
    
    	@Test
    	public void test() {
    		assertThat(MockUtil.isSpy(this.service)).isTrue();
    		doReturn(new CompletableFuture<>()).when(this.service).asyncMethod(any(Integer.class ));
    		doReturn(null).when(this.service).work(); // commenting this line causes test to pass
    		this.service.asyncMethod(1);
    		log.info("Success");
    	}
    
    }
    
    @Service
    class ExampleService {
    	
    	private static final Logger log = LoggerFactory.getLogger( ExampleService.class );
    
    	@Async // commenting this line causes test to pass
    	public CompletableFuture<Integer> asyncMethod(Integer param) {
    		work();
    		return new CompletableFuture<>();
    	}
    
    	protected Integer work() {
    		log.info("Work");
    		return 0;
    	}
    	
    }
    
    @ComponentScan
    @Configuration
    @EnableAspectJAutoProxy
    @EnableAsync(mode = AdviceMode.ASPECTJ)
    class Application {
    	
    	@Bean
    	TaskExecutor taskExector() {
    		return new ThreadPoolTaskExecutor();
    	}
    
    }
  12. removed their assignment
    on Mar 19, 2020
  13. cdalexndr commented on Mar 11, 2021

    @cdalexndr
    Author

    Still reproductible with Spring 5.3.3 (re-encountered the bug)

  14. snicoll commented on Sep 25, 2023

    @snicoll
    Member

    This looks like a more general problem, see #24735 (comment) that has a reproducer for @Cacheable with AspectJ.

  15. leogong commented on Oct 14, 2023

    @leogong
  16. DennisGoermannMD commented on Jan 17, 2024

    @DennisGoermannMD

    Hello, we have this issue too.
    Usecase:
    We want to test the caching.
    Problem:
    With a @spybean it does not work. The same test runs with an autowired bean. If the Bean is with @spybean intialized the values for the cachekey becomes null.
    Workaround:
    As workaround, we could use an autowired bean in this case. But if we do so, the (collection of) tests tooks more time. We want to have only one "base" test file with all beans initialized with @spybean, so we can mock or not in every test case. The advantage is that no spring context will be reloaded and the tests runs faster.

  17. ghilainm commented on Jan 17, 2024

    @ghilainm
  18. changed the title [-]Mockito cannot mock @Async method[/-] [+]Mockito cannot mock AspectJ-weaved @Async method[/+] on Jan 22, 2024
  19. added and removed on Feb 6, 2024
  20. added this to the General Backlog milestone on Feb 6, 2024
  21. changed the title [-]Mockito cannot mock AspectJ-weaved @Async method[/-] [+]Mockito cannot mock AspectJ-woven `@Async` method[/+] on Apr 14, 2025
  22. removed this from the General Backlog milestone on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions