Java 在单元测试中,如何在异步操作中测试方法调用

Java 在单元测试中,如何在异步操作中测试方法调用,java,asynchronous,mockito,junit5,completable-future,Java,Asynchronous,Mockito,Junit5,Completable Future,我有一个方法,它首先执行一系列操作,然后启动一个异步任务。我想测试这个方法,但我不明白如何验证异步操作已经完成 使用MoСkito,我想验证foo方法执行了2次,一次在异步任务开始之前,一次在异步任务内部。问题是,在Mockito检查时,异步任务可能还没有在异步操作中调用该方法。因此,有时进行试验,有时不进行试验 这是我的方法的示例: void testingMethod() { // some operations someObject.foo(); Completab

我有一个方法,它首先执行一系列操作,然后启动一个异步任务。我想测试这个方法,但我不明白如何验证异步操作已经完成

使用MoСkito,我想验证foo方法执行了2次,一次在异步任务开始之前,一次在异步任务内部。问题是,在Mockito检查时,异步任务可能还没有在异步操作中调用该方法。因此,有时进行试验,有时不进行试验

这是我的方法的示例:

void testingMethod() {
    // some operations
    someObject.foo();
    CompletableFuture.runAsync(() -> {
        // some other operations
        someObject.foo();
    });
}
还有我的测试示例,其中模拟了某个对象:

@Test
public void testingMethodTest() {
    testObject.testingMethod();

    Mockito.verify(someObject, Mockito.times(2)).foo();
}

是否有一种方法可以在验证方法之前等待异步操作完成。或者这是一种不好的测试方法,在这种情况下,您可以提供什么建议?

您可以使用
时间单位.SECONDS.sleep(1)
testObject.testingMethod()之后在您的测试中


另一方面,我甚至不认为您应该测试异步发生的事情是否已完成或被调用,这不是此函数的责任。

您可以有一个
时间单位.SECONDS.sleep(1)
testObject.testingMethod()之后在您的测试中


另一方面,我甚至不认为您应该测试异步发生的事情是否已完成或调用,这不是此函数的责任。

问题归结为被测试的方法调用静态方法:
CompletableFuture.runAsync()
。静态方法通常很少控制模拟和断言

即使在测试中使用
sleep()
,也不能断言是否异步调用了
someObject.foo()
。如果调用是在调用线程上进行的,则测试仍将通过。此外,使用
sleep()
会降低测试速度,而
sleep()
太短会导致测试随机失败

如果这似乎真的是唯一的解决方案,那么应该使用这样的库进行轮询,直到满足断言为止,并有一个超时

有几种方法可以使代码更易于测试:

  • Make
    testingMethod()
    返回一个
    Future
    (如您在评论中所想):这不允许断言异步执行,但可以避免等待太久
  • runAsync()
    方法包装到另一个可以模拟的服务中,并捕获参数
  • 如果您使用的是Spring,请将lambda表达式移动到另一个服务中,使用带有
    @Async
    注释的方法。这允许轻松地模拟和单元测试该服务,并消除了直接调用
    runAsync()
    的负担
  • 使用自定义执行器,并将其传递给
    runAsync()
    如果您使用的是Spring,我建议使用第三种解决方案,因为它确实是最干净的,并且避免了到处调用
    runAsync()
    而使代码混乱

    选项2和4非常相似,它只是改变了你必须模仿的东西

    如果您选择第四种解决方案,以下是您的方法:

    将测试的类更改为使用自定义执行器

    class TestedObject {
        private SomeObject someObject;
        private Executor executor;
    
        public TestedObject(SomeObject someObject, Executor executor) {
            this.someObject = someObject;
            this.executor = executor;
        }
    
        void testingMethod() {
            // some operations
            someObject.foo();
            CompletableFuture.runAsync(() -> {
                // some other operations
                someObject.foo();
            }, executor);
        }
    }
    
    实现一个自定义的执行器,它只捕获命令而不是运行命令:

    class CapturingExecutor implements Executor {
    
        private Runnable command;
    
        @Override
        public void execute(Runnable command) {
            this.command = command;
        }
    
        public Runnable getCommand() {
            return command;
        }
    }
    
    (您也可以使用
    @Mock
    执行器
    参数捕获器
    ,但我认为这种方法更干净)

    在测试中使用
    CapturingExecutor

    @RunWith(MockitoJUnitRunner.class)
    public class TestedObjectTest {
        @Mock
        private SomeObject someObject;
    
        private CapturingExecutor executor;
    
        private TestedObject testObject;
    
        @Before
        public void before() {
            executor = new CapturingExecutor();
            testObject = new TestedObject(someObject, executor);
        }
    
        @Test
        public void testingMethodTest() {
            testObject.testingMethod();
    
            verify(someObject).foo();
            // make sure that we actually captured some command
            assertNotNull(executor.getCommand());
    
            // now actually run the command and check that it does what it is expected to do
            executor.getCommand().run();
            // Mockito still counts the previous call, hence the times(2).
            // Not relevant if the lambda actually calls a different method.
            verify(someObject, times(2)).foo();
        }
    }
    

    问题归结为被测试方法调用静态方法:
    CompletableFuture.runAsync()
    。静态方法通常很少控制模拟和断言

    即使在测试中使用
    sleep()
    ,也不能断言是否异步调用了
    someObject.foo()
    。如果调用是在调用线程上进行的,则测试仍将通过。此外,使用
    sleep()
    会降低测试速度,而
    sleep()
    太短会导致测试随机失败

    如果这似乎真的是唯一的解决方案,那么应该使用这样的库进行轮询,直到满足断言为止,并有一个超时

    有几种方法可以使代码更易于测试:

  • Make
    testingMethod()
    返回一个
    Future
    (如您在评论中所想):这不允许断言异步执行,但可以避免等待太久
  • runAsync()
    方法包装到另一个可以模拟的服务中,并捕获参数
  • 如果您使用的是Spring,请将lambda表达式移动到另一个服务中,使用带有
    @Async
    注释的方法。这允许轻松地模拟和单元测试该服务,并消除了直接调用
    runAsync()
    的负担
  • 使用自定义执行器,并将其传递给
    runAsync()
    如果您使用的是Spring,我建议使用第三种解决方案,因为它确实是最干净的,并且避免了到处调用
    runAsync()
    而使代码混乱

    选项2和4非常相似,它只是改变了你必须模仿的东西

    如果您选择第四种解决方案,以下是您的方法:

    将测试的类更改为使用自定义执行器

    class TestedObject {
        private SomeObject someObject;
        private Executor executor;
    
        public TestedObject(SomeObject someObject, Executor executor) {
            this.someObject = someObject;
            this.executor = executor;
        }
    
        void testingMethod() {
            // some operations
            someObject.foo();
            CompletableFuture.runAsync(() -> {
                // some other operations
                someObject.foo();
            }, executor);
        }
    }
    
    实现一个自定义的执行器,它只捕获命令而不是运行命令:

    class CapturingExecutor implements Executor {
    
        private Runnable command;
    
        @Override
        public void execute(Runnable command) {
            this.command = command;
        }
    
        public Runnable getCommand() {
            return command;
        }
    }
    
    (您也可以使用
    @Mock
    执行器
    参数捕获器
    ,但我认为这种方法更干净)

    在测试中使用
    CapturingExecutor

    @RunWith(MockitoJUnitRunner.class)
    public class TestedObjectTest {
        @Mock
        private SomeObject someObject;
    
        private CapturingExecutor executor;
    
        private TestedObject testObject;
    
        @Before
        public void before() {
            executor = new CapturingExecutor();
            testObject = new TestedObject(someObject, executor);
        }
    
        @Test
        public void testingMethodTest() {
            testObject.testingMethod();
    
            verify(someObject).foo();
            // make sure that we actually captured some command
            assertNotNull(executor.getCommand());
    
            // now actually run the command and check that it does what it is expected to do
            executor.getCommand().run();
            // Mockito still counts the previous call, hence the times(2).
            // Not relevant if the lambda actually calls a different method.
            verify(someObject, times(2)).foo();
        }
    }
    

    对于那些你正在寻找的人