spring-boot version is 2.1.3.RELEASE I already addressed the issue in mockito. issue the mockito 3.4.0 can mock static method, so I upgrade mockito to 3.4.0, but the mock framework changed, now when I use the @SpyBean and then verify it. it will throw NotAMockException.

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
@AutoConfigureMockMvc
public class AppGroupClusterActionTest {

  @Autowired
  private MockMvc mockMvc;

  @MockBean
  private AppGroupClusterService appGroupClusterService;

  @SpyBean
  private AppGroupClusterAction appGroupClusterAction;

  @Captor
  private ArgumentCaptor<Object[]> argumentCaptor;

  @Test
  public void testUpdateExecuteAuditSpel() throws Exception {
    ServiceResult mockRes = new ServiceResult();
    mockRes.setSuccess(true);
    doReturn(mockRes).when(appGroupClusterService).update(
        Mockito.any(), Mockito.any(), Mockito.any(),
        Mockito.any(), Mockito.any(), Mockito.any(),
        Mockito.any(), Mockito.any(), Mockito.any());

    mockMvc.perform(
        MockMvcRequestBuilders.post("/platforms/{platform}/appGroupNames/{appGroupName}/clusterNames/{clusterName}/update",
            "platform", "appGroupName", "clusterName").content("{}"))
        .andExpect(status().isOk())
        .andReturn();

//    not work
//    verify(appGroupClusterAction).addAfterAuditResult(argumentCaptor.capture());
    verify((AppGroupClusterAction) AopTestUtils.getTargetObject(appGroupClusterAction)).addAfterAuditResult(argumentCaptor.capture());
    assertArrayEquals(argumentCaptor.getValue(), new Object[]{"appGroupName", "platform", "clusterName", "{}"});

    verify((AppGroupClusterAction) AopTestUtils.getTargetObject(appGroupClusterAction)).addAfterAuditResult(argumentCaptor.capture());
    assertArrayEquals(argumentCaptor.getValue(), new Object[]{"appGroupName", "platform", "clusterName", "{}"});
  }
}

Comment From: wilkinsona

@jacky1193610322 Thanks for the report. We don't officially support Mockito 3.4 yet, but it's likely that we will want to do so in Spring Boot 2.4. To help us to investigate your problem, can you please provide a minimal sample that reproduces it? 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: spring-projects-issues

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.

Comment From: jacky1193610322

sorry,I will offer simple code,but because of the rule of company, I can't upload the whole project, I will paste the core code.

Comment From: snicoll

@jacky1193610322 it doesn't have to be your project. Actually, it has to be a minimal sample and that shouldn't expose any company specific information. Please don't paste the core code as the only thing we could do is try to copy/paste that in an actual project we can run.

Comment From: jacky1193610322

I know. but push the code to GitHub is forbid.

Comment From: snicoll

You can share the code in a different form (attaching a zip to this issue) or you can send it to me by email if that's easier. My email is on my github profile.

Comment From: jacky1193610322

ok

Comment From: jacky1193610322

sorry, the mail can't work now, can't upload code, but the example is simple, any aop proxy can reproduction. Aspect

@Component
@org.aspectj.lang.annotation.Aspect
public class Aspect {
  @Pointcut("execution(* com.example.demo.*.*(..))")
  public void pointcut() {
  }

  @Around("pointcut()")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
      return joinPoint.proceed();
    } catch (Throwable e) {
      throw e;
    } finally {
      System.out.println("around");
    }
  }
}

http action

@RequestMapping(value = "/demo/", produces = MediaType.APPLICATION_JSON_VALUE)
@RestController
public class Action {
  public void test() {

  }
}

test

@SpringBootTest
class ActionTest {
  @SpyBean
  private Action action;

  @Test
  void name() {
    doNothing().when(action).test();
    Mockito.verify(action);
  }
}

Comment From: wilkinsona

@jacky1193610322 Your example above works for me with Mockito 3.4. I modified it slightly to correct the verification. This left the test looking like this:

@SpringBootTest
class ActionTest {
  @SpyBean
  private Action action;

  @Test
  void name() {
    doNothing().when(action).test();
    action.test();
    verify(action).test();
  }
}

It passes, outputting around to System.out numerous times as it does so.

Without a sample that reproduces the problem that you're seeing, I'm afraid we won't be able to make any progress here.

Comment From: jacky1193610322

Thank you. this is my build.gradle

plugins {
    id 'org.springframework.boot' version '2.3.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    compile('org.aspectj:aspectjweaver:1.9.2')
    testCompile "org.mockito:mockito-core:3.4.4"
    testCompile "org.mockito:mockito-inline:3.4.4"
}

test {
    useJUnitPlatform()
}

you need add testCompile "org.mockito:mockito-inline:3.4.4", this dependency can enable static mock.

Comment From: wilkinsona

Thanks. I've reproduced the problem now. It isn't specific to Mockito 3.4.x. The same failure occurs with Mockito 3.3.3 and 3.1.0 with a dependency on org.mockito:mockito-inline.

Comment From: wilkinsona

Looking more closely, I think this is a Mockito bug or at least a limitation. As a result of using @SpyBean a Mockito spy is created for Action. This spy is then proxied by Spring's AOP infrastructure and it's this proxy that is injected into the test class. It's then passed into a call to when at which point Mockito checks that it's a mock by checking that is has a non-null MockHandler. This works when using the standard mock maker but fails when using the inline mock maker. In the working case, SubclassByteBuddyMockMaker retrieves the MockHandler as follows:

if (!(mock instanceof MockAccess)) {
    return null;
}
return ((MockAccess) mock).getMockitoInterceptor().getMockHandler();

mock is an instance of MockAccess and the handler from the interceptor is returned.

In the failing case, InlineByteBuddyMockMaker attempts to retrieve the MockHandler has follows:

MockMethodInterceptor interceptor = mocks.get(mock);
if (interceptor == null) {
    return null;
} else {
    return interceptor.handler;
}

mocks.get(mock) returns null so null is returned.

Given the same input to their getHandler methods, I would expect InlineByteBuddyMockMaker and SubclassByteBuddyMockMaker to return the same output, hence the feeling that this is a Mockito bug or at least a limitation.

The problem can be worked around by removing Spring's proxy before passing the spy into Mockito:

Action actionSpy = AopTestUtils.<Action>getTargetObject(action);
doNothing().when(actionSpy).test();
action.test();
verify(actionSpy).test();

Comment From: raphw

Cross-posting from our issue tracker: TLNR; this is not a Mockito bug, it's Spring relying on an undocumented implementation detail that might just change in any bugfix release.

The reason things currently (!) work with the SubclassByteBuddyMockMaker is that Spring proxies the (internal) MockAccess interface which we implement in the mock to extract the mock state which is stored in a field of the mock instance. We could just as much had read that field directly to avoid the interface; and this wouldn't even be such a bad idea as it less pollutes the reflection API. But at this time, when we call our internal interface method, it gets redirected to the actual mock object by Spring which is why this functions (accidentally).

The reason it does not work with the InlineByteBuddyMockMaker is that we cannot implement interfaces in a class after it has been loaded. The whole premise of the inline mock maker is to keep changes invisible to make use of the JVMs retransformation facilities even after class loading which makes this a requirement. Therefore, we need to identify mocks by the object's identity and store the mock state externally in a weak map (which requires using object identity), which is not the same for the Spring proxy object. We cannot change this and from our perspective nothing is broken.

From Mockito's side things are simple: if one does not provide "our" mock object to "our" API, we do not guarantee to function, not in the past, nor the future. That it does currently work with the subclass mock maker facility is rather accidental and might change in the future. If you implemented the MockAccess interface yourself and returned another object's mock state from it and passed this to Mockito, you'd break Mockito, too. If Spring cannot do anything about this, users need to unproxy the objects explicitly but there is nothing we can do about it. A proxied mock is not a mock from our side, just some object with a reference to a mock and of those there are of course plenty.

Comment From: wilkinsona

It looks like we'll be able to address this with a custom MockResolver that builds on the change that @raphw has prototyped for https://github.com/mockito/mockito/issues/1980.

Comment From: raphw

Great. new Mockito release to Central is underway just now.

Comment From: TimvdLippe

I just realized we actually didn't publish a new minor version to Maven Central. However, I just tried to publish 3.6.0 and Bintray had issues. Hopefully we can figure out soon how to resolve that.

Comment From: TimvdLippe

We have been able to fix our release woes and 3.6.0 has been published to Maven Central: https://repo1.maven.org/maven2/org/mockito/mockito-core/3.6.0/

Comment From: snicoll

Thanks @TimvdLippe. The upgrade is scheduled for 2.4.0-RC1, see #23924

Comment From: wilkinsona

Closed by d9084ea.

Comment From: elxnis

Hi @wilkinsona,

I think org.springframework.test.util.AopTestUtils#getUltimateTargetObject causes issues with scoped beans, similarly as before in https://github.com/spring-projects/spring-boot/issues/19020. I will try to test with my sample from that issue and raise new case if it is failing again.

Thanks, Reinis