Open cdalexndr opened 5 years ago
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?
We've seen a few issue like this in the past related to AOP. I'll transfer the issue.
@cdalexndr Could you please provide a sample that shows the problem. Preferably something we can just download or clone and run.
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.
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)
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.
@mbhave I've tried and failed to reproduce the problem without @SpyBean
. Do you still have the code that you used anywhere?
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" );
}
}
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.
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();
}
}
Still reproductible with Spring 5.3.3 (re-encountered the bug)
This looks like a more general problem, see https://github.com/spring-projects/spring-framework/issues/24735#issuecomment-601172062 that has a reproducer for @Cacheable
with AspectJ.
I have the same problem. Spy a bean which is a AOP proxy bean, same error occured.
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.
Same issue here. @SpyBean with @Cacheable does not work.
When trying to mock an
@Async
method from a@SpyBean
I get the following error: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)