alibaba / testable-mock

换种思路写Mock,让单元测试更简单
https://alibaba.github.io/testable-mock/
MIT License
1.82k stars 309 forks source link

如果被测方法是异步调用,则 withTimes 不生效 #304

Closed oriming closed 1 year ago

oriming commented 1 year ago
  1. 被测方法:

    @Service
    public class LayoutServiceImpl  implements LayoutService {
    
    // 被测方法
    @Override
    @Async(ThreadPoolConstant.XXX_POOL_NAME)
    public void asyncParallelExec() {
       asyncExecImmediately();
    }
    
    @Override
    @Async(ThreadPoolConstant.XXX_POOL_NAME)
    public void asyncExecImmediately() {
        System.out.println("asyncExecImmediately");
    }
    }
  2. 测试方法:

    @Slf4j
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class LayoutServiceImplTest extends BaseFakerTest {
    
     @Autowired
     LayoutServiceImpl service;
    
     public static class Mock {
        @MockInvoke(targetClass = LayoutServiceImpl.class)
        public void asyncExecImmediately() {
        log.warn("mock invoke [asyncExecImmediately] of LayoutServiceImpl");
        }
    }
    
    @Test
    public void asyncParallelExec() {
        assertDoesNotThrow(() -> service.asyncParallelExec());
        verifyInvoked("asyncExecImmediately").withTimes(1);
    }
    }
  3. Console 输出

    
    2022-11-24 10:34:47.379  WARN 18294 [] --- [k-exec-thread-1] .s.a.s.s.i.LayoutServiceImplTest : mock invoke [asyncExecImmediately] of LayoutServiceImpl

com.alibaba.testable.core.error.VerifyFailedError: Expected times: 1 Actual times: 0

linfan commented 1 year ago

问题收到,感谢详细的代码示例,我先尝试复现一下看看

linfan commented 1 year ago

这个问题确实有点神奇... 排查了好久最后发现原因其实就在眼前🥲

由于@Async的作用,实际Mock方法的调用其实是发生在verifyInvoked()之后的,所以在做withTimes(1)断言的时候,这个Mock方法的调用次数确实依然是0:

@Test
public void asyncParallelExec() {
    assertDoesNotThrow(() -> service.asyncParallelExec());     //  这行先执行
    verifyInvoked("asyncExecImmediately").withTimes(1);        //  然后开始做断言
                                                               //  在这之后,asyncParallelExec()方法里面的asyncExecImmediately()调用才发生
}

只需要在verifyInvoked()和Mock方法里同时加上断点执行测试就能发现其中的“奥妙”。

解决办法也很简单,在verifyInvoked("asyncExecImmediately").withTimes(1);前面加上一行Thread.sleep(1000);,测试通过🤣

linfan commented 1 year ago

顺带一提,对于在方法内显示使用了异步回调的调用(比如Mock在CompletableFuture.supplyAsync()回调里的方法调用),还需要开启Testable封装的transmittable-thread-local库针对异步线程的字节码增强逻辑,具体方法见Mock线程池内的调用,这种情况很少遇到,通常可以忽略。

oriming commented 1 year ago

@linfan 原来如此,感谢😂