如何使用 mockito 测试运行异步线程的方法

问题描述

我有以下代码要测试:

@Slf4j
@Component
public class dispatcherTask {

    private final MyClassService myClassService;
    private final SreamingService streamingService;
    private ExecutorService executor = Executors.newCachedThreadPool();
    private final Set < String > dispatching = new ConcurrentSkipListSet < > ();
    private final RetryPolicy < Object > httpRetryPolicy;

    public dispatcherTask(MyClassService myClassService,SreamingService streamingService) {
        this.myClassService = myClassService;
        this.streamingService = streamingService;
        this.httpRetryPolicy = new RetryPolicy < > ()
            .handle(HttpClientErrorException.class)
            .onRetriesExceeded(e - > log.warn("Max retries have been exceeded for http exception"))
            .withBackoff(2,3,ChronoUnit.SECONDS)
            .withMaxRetries(5);
    }

    public void initMethod(MyClass myclass) {
        Failsafe.with(httpRetryPolicy).with(executor)
            .onFailure((e) - > {
                // Todo log.error();
                myClassService.updateStatus(myclass.getId(),Status.Failed);
            })
            .onSuccess((o) - > {
                MyClass updatedMyClass = myClassService.updateStatus(myclass.getId(),GameStatus.ENDED);
                streamingService.storeData(updateMyClass);
                dispatching.remove(myclass.getPlatformId());
                //Todo log.info()
            })
            .runAsync(() - > {
                MyClass updatedMyClass =
                myClassService.updateStatus(myclass.getId(),Status.STREAMING);
                streamingService.polling(StreamedClass.builder().myclass(updatedMyClass).build());
                // Todo log.info()
            });
    }
}

这是我使用 Mockito 的 JUnit 测试:

@ExtendWith(MockitoExtension.class)
public class dispatcherTaskTest {

    @Mock private MyClassService myClassService;
    @Mock private StreamingService streamingService;
    @InjectMocks private dispatcherTask dispatcherTask;

    @Test
    void test() throws InterruptedException {
        //given
        MyClass myclass = new MyClass().setId(1L).setName("name").setStatus(Status.CREATED);
        when(myClassService.updateStatus(myclass.getId(),Status.STREAMING)).thenReturn(myclass);
        when(myClassService.updateStatus(myclass.getId(),Status.ENDED)).thenReturn(myclass);
        donothing().when(streamingService).polling(any());
        donothing().when(streamingService).storeData(any());
        //when
        dispatcherTask.initMethod(myclass)
        //then
        verify(myClassService,times(1)).updateStatus(myclass.getId(),Status.STREAMING);
        verify(myClassService,Status.ENDED);
    }
}

如果我像这样运行它,检查状态 ENDED 的最后一次验证失败。如果我添加一个 Thread.sleep(3l); 它通过。是否有更好或更安全的方法来通过测试而不添加 sleep() 方法

解决方法

两种可行的方法

1 - 在 Dispatcher 中添加 shutdownawait 方法

您可以在 Dispatcher 类中添加 shutdownawaitTermination 方法。 (如果需要,您可以统一它们)

public class DispatcherTask 
{    
  //...
  public void shutdownExecutor()
  {
     executor.shutdown();
  }
  public void waitToFinish(long seconds)
  {
     executor.awaitTermination(seconds,TimeUnit.SECONDS);
  }
  //...
}

boolean awaitTermination

阻塞直到所有任务在关机后完成执行 请求,或发生超时,或当前线程中断, 以先发生者为准。


在您的 Dispatcher 中使用此方法,可以更改测试,以便您可以:

@Test
void test() throws InterruptedException {
    //...
    dispatcherTask.initMethod(myclass);
    verify(myClassService,times(1)).updateStatus(myclass.getId(),Status.STREAMING);
   
    dispatcherTask.shutdownExecutor();
    dispatcherTask.waitToFinish(3L); //this will block without manually sleeping
    verify(myClassService,Status.ENDED);
}

2 - 添加 getExecutor 方法并在测试中调用 shutdown/await

public class DispatcherTask 
{    
  //...
  public ExecutorService getExecutor()
  {
     return executor;
  }
  //...
}

在你的测试中:

@Test
void test() throws InterruptedException {
    //...
     dispatcherTask.initMethod(myclass);
     verify(myClassService,Status.STREAMING);
    
     //create a variable in the test or just invoke getExecutor
     dispatcherTask.getExecutor().shutdown();
     dispatcherTask.getExecutor().awaitTermination(3L,TimeUnit.SECONDS); 
     verify(myClassService,Status.ENDED);
}
,

测试 Failsafe 的策略将很困难,例如:您将如何模拟一个测试,该测试表示:“我将重试 3 次,第 4 次我将成功。” ,因为你确实有withMaxRetries(5)。为此,我建议您阅读 thenAnswer 中的 Mockito 的作用,而不是 thenReturn。至少这是我们测试使用 Failsafe 的代码的方式。

不过,对于您的问题,只需使用 dependency injection(人们认为它只出现在 Spring 中)。因此,将您的代码重新编写为:

 public DispatcherTask(MyClassService myClassService,StreamingService streamingService,ExecutorService executor ) {

请注意,您的 ExecutorService 现在已在构造函数中传递。现在创建一个真实的 DispatcherTask 实例,并模拟它的所有依赖项:

 DispatcherTask dt = new DispatcherTask(
       myClassService,// this one is a Mock
       streamingService,// this one is a Mock
       someExecutorService   // and what is this?
 );

什么是someExecutorService?那么它是一个 ExecutorService,它将在运行单元测试的同一线程中执行您的代码。例如,选择任意 from here。请记住,ExecutorService 是一个 interface,因此创建在同一线程中运行的实现是微不足道的,例如 this answer did

,

我什至不尝试在单元测试中管理其他线程。我不确定您的示例中 executor 的来源,但我会在您的测试中注入一个在当前线程上执行的版本。 Guava 从 com.google.common.util.concurrent.MoreExecutors::directExecutor 返回一个。