如何使用JUnit测试异步进程


204

您如何测试使用JUnit触发异步过程的方法?

我不知道如何让我的测试等待流程结束(它不完全是单元测试,它更像是集成测试,因为它涉及多个类,而不仅仅是一个类)。


您可以尝试JAT(Java异步测试):bitbucket.org/csolar/jat
cs0lar 2013年

2
JAT拥有1位观看者,并且1.5年未更新。Awaitility仅在1个月前更新,并且在撰写本文时为1.6版。我与这两个项目都不隶属,但是如果我要投资于我的项目的附加项目,那么我现在会更加信任Awaitility。
Les Hazlewood 2014年

JAT仍无更新:“最新更新2013-01-19”。只需节省时间以跟随链接。
迪蒙

@LesHazlewood,一个观察者对JAT不利,但多年来一直没有更新……仅是一个例子。如果它能正常工作,您多久更新一次操作系统的低级TCP堆栈?可以在stackoverflow.com/questions/631598/…以下回答JAT的替代方法。
user1742529

Answers:


46

恕我直言,让单元测试创​​建或在线程上等待是不好的做法。您希望这些测试能在几秒钟内运行。这就是为什么我想提出一种分两步的方法来测试异步过程。

  1. 测试您的异步过程是否已正确提交。您可以模拟接受异步请求的对象,并确保提交的作业具有正确的属性,等等。
  2. 测试您的异步回调在做正确的事情。在这里,您可以模拟出最初提交的作业,并假设它已正确初始化,并验证您的回调正确。

148
当然。但是有时您需要测试专门用于管理线程的代码。
缺少2011年

77
对于使用Junit或TestNG进行集成测试(而不仅仅是单元测试)或用户接受测试(例如,带Cucumber)的那些人,绝对需要等待异步完成并验证结果。
Les Hazlewood 2014年

38
异步过程是最复杂的一些代码,您说您不应该对它们使用单​​元测试,而应该仅使用单个线程进行测试?那是一个非常糟糕的主意。
查尔斯(Charles)

18
模拟测试通常无法证明功能是端到端的。需要以异步方式测试异步功能,以确保其正常工作。如果愿意,可以将其称为集成测试,但这仍然是需要的测试。
Scott Boring 2015年

4
这不应是公认的答案。测试超越了单元测试。OP称其为集成测试而非单元测试。
耶利米亚当斯

190

一种替代方法是使用CountDownLatch类。

public class DatabaseTest {

    /**
     * Data limit
     */
    private static final int DATA_LIMIT = 5;

    /**
     * Countdown latch
     */
    private CountDownLatch lock = new CountDownLatch(1);

    /**
     * Received data
     */
    private List<Data> receiveddata;

    @Test
    public void testDataRetrieval() throws Exception {
        Database db = new MockDatabaseImpl();
        db.getData(DATA_LIMIT, new DataCallback() {
            @Override
            public void onSuccess(List<Data> data) {
                receiveddata = data;
                lock.countDown();
            }
        });

        lock.await(2000, TimeUnit.MILLISECONDS);

        assertNotNull(receiveddata);
        assertEquals(DATA_LIMIT, receiveddata.size());
    }
}

注意您不能只使用与常规对象同步的锁,因为快速回调可以在调用锁的wait方法之前释放锁。请参阅Joe Walnes撰写的博客文章。

编辑感谢@jtahlborn和@Ring的评论,删除了CountDownLatch周围的同步块


8
请不要遵循此示例,这是不正确的。您应该在CountDownLatch上进行同步,因为它在内部处理线程安全性。
jtahlborn 2012年

1
在同步部分之前,这是一个很好的建议,这可能会花费接近3-4个小时的调试时间。stackoverflow.com/questions/11007551/…– 2012

2
对错误表示歉意。我已经适当地编辑了答案。
马丁

7
如果要验证是否已调用onSuccess,则应断言lock.await返回true。
吉尔伯特2014年

1
@Martin是正确的,但这意味着您有其他问题需要解决。
乔治·阿里斯蒂

75

您可以尝试使用Awaitility库。它使测试您正在谈论的系统变得容易。


21
友好的免责声明:Johan是该项目的主要贡献者。
dbm

1
受制于必须等待的根本问题(单元测试需要快速运行)。理想情况下,您真的不想等待超过所需时间的毫秒,因此我认为CountDownLatch在这方面使用(参见@Martin的回答)更好。
乔治·阿里斯蒂

非常棒。
R. Karlus

这是完美的库,可以满足我的异步流程集成测试要求。非常棒。该库似乎维护得很好,并且具有从基本到高级的扩展功能,我认为这些功能足以满足大多数情况。感谢您提供的参考!
Tanvir

真的很棒的建议。谢谢
RoyalTiger

68

如果使用CompletableFuture(Java 8中引入)或SettableFuture(来自Google Guava),则可以在测试完成后立即完成测试,而不必等待预定的时间。您的测试如下所示:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());

4
...并且如果您被Java少于8所困扰,请尝试做同样事情的guavas SettableFuture
Markus T


18

我发现一种对测试异步方法非常有用的方法是注入一个 Executor在要测试的对象的构造函数中实例。在生产中,将执行程序实例配置为异步运行,而在测试中,可以将其模拟为同步运行。

因此,假设我正在尝试测试异步方法Foo#doAsync(Callback c)

class Foo {
  private final Executor executor;
  public Foo(Executor executor) {
    this.executor = executor;
  }

  public void doAsync(Callback c) {
    executor.execute(new Runnable() {
      @Override public void run() {
        // Do stuff here
        c.onComplete(data);
      }
    });
  }
}

在生产中,我将Foo使用Executors.newSingleThreadExecutor()Executor实例进行构建,而在测试中,我可能将使用执行以下操作的同步执行器进行构建-

class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable r) {
    r.run();
  }
}

现在我对异步方法的JUnit测试非常干净-

@Test public void testDoAsync() {
  Executor executor = new SynchronousExecutor();
  Foo objectToTest = new Foo(executor);

  Callback callback = mock(Callback.class);
  objectToTest.doAsync(callback);

  // Verify that Callback#onComplete was called using Mockito.
  verify(callback).onComplete(any(Data.class));

  // Assert that we got back the data that we expected.
  assertEquals(expectedData, callback.getData());
}

如果我要集成测试涉及异步库调用(例如Spring)的东西,这是行不通的WebClient
Stefan Haberl

8

测试线程/异步代码本质上没有错,特别是如果线程是您要测试的代码的重点。测试这些东西的一般方法是:

  • 阻塞主测试线程
  • 从其他线程捕获失败的断言
  • 解除阻塞主测试线程
  • 重新抛出任何故障

但这是一个测试的样板。更好/更简单的方法是只使用ConcurrentUnit

  final Waiter waiter = new Waiter();

  new Thread(() -> {
    doSomeWork();
    waiter.assertTrue(true);
    waiter.resume();
  }).start();

  // Wait for resume() to be called
  waiter.await(1000);

相对于该CountdownLatch方法,这样做的好处是它比较简单,因为在任何线程中发生的断言失败都会适当地报告给主线程,这意味着测试应该在适当的时候失败。是比较书面记录CountdownLatch的方式来ConcurrentUnit是这里

我还为那些想了解更多详细信息的人写了一篇关于该主题的博客文章


我已经在过去使用类似的解决方案是github.com/MichaelTamm/junit-toolbox,也是特色作为一个第三方扩展junit.org/junit4
dschulten

4

如何进行调用SomeObject.waitnotifyAll并按此处所述进行操作或者使用Robotiums Solo.waitForCondition(...)方法进行操作,或者使用我编写来做到这一点(有关用法,请参见注释和测试类)


1
等待/通知/中断方法的问题在于,您正在测试的代码可能会干扰等待线程(我已经看到了这种情况)。这就是为什么ConcurrentUnit使用线程可以等待的专用电路的原因,该专用电路不会因主测试线程的中断而无意中受到干扰。
乔纳森(Jonathan)2015年

3

我找到了一个库socket.io来测试异步逻辑。使用LinkedBlockingQueue看起来很简单。这是示例

    @Test(timeout = TIMEOUT)
public void message() throws URISyntaxException, InterruptedException {
    final BlockingQueue<Object> values = new LinkedBlockingQueue<Object>();

    socket = client();
    socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
        @Override
        public void call(Object... objects) {
            socket.send("foo", "bar");
        }
    }).on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            values.offer(args);
        }
    });
    socket.connect();

    assertThat((Object[])values.take(), is(new Object[] {"hello client"}));
    assertThat((Object[])values.take(), is(new Object[] {"foo", "bar"}));
    socket.disconnect();
}

使用LinkedBlockingQueue采取API进行阻塞,直到获得结果为止,就像同步方式一样。并设置超时以避免假设花费太多时间等待结果。


1
很棒的方法!
传记

3

值得一提的是Testing Concurrent Programs,“ 并发实践”中有一章非常有用,它描述了一些单元测试方法并提供了解决问题的方法。


1
那是哪种方法?你能举个例子吗?
布鲁诺·费雷拉

2

如果测试结果是异步产生的,这就是我现在正在使用的。

public class TestUtil {

    public static <R> R await(Consumer<CompletableFuture<R>> completer) {
        return await(20, TimeUnit.SECONDS, completer);
    }

    public static <R> R await(int time, TimeUnit unit, Consumer<CompletableFuture<R>> completer) {
        CompletableFuture<R> f = new CompletableFuture<>();
        completer.accept(f);
        try {
            return f.get(time, unit);
        } catch (InterruptedException | TimeoutException e) {
            throw new RuntimeException("Future timed out", e);
        } catch (ExecutionException e) {
            throw new RuntimeException("Future failed", e.getCause());
        }
    }
}

使用静态导入,该测试看起来不错。(请注意,在这个示例中,我正在启动一个线程来说明这个想法)

    @Test
    public void testAsync() {
        String result = await(f -> {
            new Thread(() -> f.complete("My Result")).start();
        });
        assertEquals("My Result", result);
    }

如果f.complete未调用,则在超时后测试将失败。您还可以使用f.completeExceptionally早期失败。


2

这里有很多答案,但是一个简单的答案就是创建一个完整的CompletableFuture并使用它:

CompletableFuture.completedFuture("donzo")

所以在我的测试中:

this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));

我只是确保所有这些东西都会被调用。如果您使用以下代码,则此技术有效:

CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();

当所有CompletableFutures完成时,它将在其中快速滑动!


2

尽可能避免使用并行线程进行测试(大多数情况下)。这只会使您的测试不稳定(有时会通过,有时会失败)。

仅在需要调用某些其他库/系统时,才可能需要等待其他线程,在这种情况下,请始终使用Awaitility库而不是Thread.sleep()

永远不要只是打电话get()join()在您的测试中进行,否则您的测试可能会在CI服务器上永远运行,以防万一将来无法完成。isDone()在致电之前,请始终在测试中首先断言get()。对于CompletionStage,即.toCompletableFuture().isDone()

当您测试这样的非阻塞方法时:

public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
    return future.thenApply(result -> "Hello " + result);
}

那么您不仅应该通过在测试中传递完整的Future来测试结果,还应确保方法doSomething()不会通过调用join()或来阻止get()。这一点特别重要,如果您使用非阻塞框架。

为此,请测试您设置为手动完成的未完成的将来:

@Test
public void testDoSomething() throws Exception {
    CompletableFuture<String> innerFuture = new CompletableFuture<>();
    CompletableFuture<String> futureResult = createGreeting(innerFuture).toCompletableFuture();
    assertFalse(futureResult.isDone());

    // this triggers the future to complete
    innerFuture.complete("world");
    assertTrue(futureResult.isDone());

    // futher asserts about fooResult here
    assertEquals(futureResult.get(), "Hello world");
}

这样,如果您添加future.join()到doSomething(),则测试将失败。

如果您的服务使用诸如中的ExecutorService thenApplyAsync(..., executorService),则在您的测试中注入单线程ExecutorService,例如来自guava的ExecutorService:

ExecutorService executorService = Executors.newSingleThreadExecutor();

如果您的代码使用了forkForinPool之类的thenApplyAsync(...),请重写该代码以使用ExecutorService(有很多充分的理由)或使用Awaitility。

为了简化示例,我将BarService设置为在测试中实现为Java8 lambda的方法参数,通常它是您要模拟的注入引用。


嘿@tkruse,也许您有使用此技术进行测试的公共git repo?
克里斯蒂亚诺

@克里斯蒂亚诺:那将违反SO哲学。相反,当将它们粘贴到空的junit测试类中时,我将其更改为无需任何其他代码即可编译的方法(所有导入均为java8 +或junit)。随时支持。
tkruse

我现在明白了 谢谢。我现在的问题是测试方法何时返回CompletableFuture,但是否接受其他对象作为CompletableFuture以外的参数。
克里斯蒂亚诺

在您的情况下,谁创建该方法返回的CompletableFuture?如果这是另一项服务,那可以被嘲笑,我的技术仍然适用。如果该方法本身创建了CompletableFuture,则情况会发生很大变化,因此您可以提出一个新的问题。然后,取决于哪个线程将完成方法返回的将来。
tkruse

1

我更喜欢使用等待和通知。简单明了。

@Test
public void test() throws Throwable {
    final boolean[] asyncExecuted = {false};
    final Throwable[] asyncThrowable= {null};

    // do anything async
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                // Put your test here.
                fail(); 
            }
            // lets inform the test thread that there is an error.
            catch (Throwable throwable){
                asyncThrowable[0] = throwable;
            }
            // ensure to release asyncExecuted in case of error.
            finally {
                synchronized (asyncExecuted){
                    asyncExecuted[0] = true;
                    asyncExecuted.notify();
                }
            }
        }
    }).start();

    // Waiting for the test is complete
    synchronized (asyncExecuted){
        while(!asyncExecuted[0]){
            asyncExecuted.wait();
        }
    }

    // get any async error, including exceptions and assertationErrors
    if(asyncThrowable[0] != null){
        throw asyncThrowable[0];
    }
}

基本上,我们需要创建一个最终的Array引用,以在匿名内部类内部使用。我宁愿创建一个boolean [],因为如果需要wait(),可以放置一个值来控制。完成所有操作后,我们只需释放asyncExecuted。


1
如果您的断言失败,则主测试线程将不知道它。
乔纳森

感谢您提供解决方案,可帮助我调试Websocket连接的代码。
Nazar Sakharenko '16

@Jonathan,我更新了代码以捕获任何断言和异常,并将其告知主测试线程。
Paulo

1

对于那里的所有Spring用户,这是我如今通常进行集成测试的方式,其中涉及到异步行为:

异步任务(例如I / O调用)完成后,在生产代码中触发应用程序事件。无论如何,在大多数情况下,必须使用此事件来处理生产中异步操作的响应。

有了此事件,您就可以在测试案例中使用以下策略:

  1. 执行被测系统
  2. 收听事件并确保事件已触发
  3. 做你的断言

为了解决这个问题,您首先需要触发某种域事件。我在这里使用UUID来标识已完成的任务,但是您当然可以随意使用其他东西,只要它是唯一的即可。

(请注意,以下代码段也使用Lombok注释来摆脱样板代码)

@RequiredArgsConstructor
class TaskCompletedEvent() {
  private final UUID taskId;
  // add more fields containing the result of the task if required
}

生产代码本身通常如下所示:

@Component
@RequiredArgsConstructor
class Production {

  private final ApplicationEventPublisher eventPublisher;

  void doSomeTask(UUID taskId) {
    // do something like calling a REST endpoint asynchronously
    eventPublisher.publishEvent(new TaskCompletedEvent(taskId));
  }

}

然后,我可以使用Spring @EventListener在测试代​​码中捕获已发布的事件。事件侦听器会涉及更多一点,因为它必须以线程安全的方式处理两种情况:

  1. 生产代码比测试案例要快,并且在测试案例检查事件之前已经触发了该事件,或者
  2. 测试用例比生产代码快,并且测试用例必须等待事件。

CountDownLatch如此处其他答案中所述,A 用于第二种情况。还要注意,@Order事件处理程序方法上的注释可确保在生产中使用的任何其他事件侦听器之后调用此事件处理程序方法。

@Component
class TaskCompletionEventListener {

  private Map<UUID, CountDownLatch> waitLatches = new ConcurrentHashMap<>();
  private List<UUID> eventsReceived = new ArrayList<>();

  void waitForCompletion(UUID taskId) {
    synchronized (this) {
      if (eventAlreadyReceived(taskId)) {
        return;
      }
      checkNobodyIsWaiting(taskId);
      createLatch(taskId);
    }
    waitForEvent(taskId);
  }

  private void checkNobodyIsWaiting(UUID taskId) {
    if (waitLatches.containsKey(taskId)) {
      throw new IllegalArgumentException("Only one waiting test per task ID supported, but another test is already waiting for " + taskId + " to complete.");
    }
  }

  private boolean eventAlreadyReceived(UUID taskId) {
    return eventsReceived.remove(taskId);
  }

  private void createLatch(UUID taskId) {
    waitLatches.put(taskId, new CountDownLatch(1));
  }

  @SneakyThrows
  private void waitForEvent(UUID taskId) {
    var latch = waitLatches.get(taskId);
    latch.await();
  }

  @EventListener
  @Order
  void eventReceived(TaskCompletedEvent event) {
    var taskId = event.getTaskId();
    synchronized (this) {
      if (isSomebodyWaiting(taskId)) {
        notifyWaitingTest(taskId);
      } else {
        eventsReceived.add(taskId);
      }
    }
  }

  private boolean isSomebodyWaiting(UUID taskId) {
    return waitLatches.containsKey(taskId);
  }

  private void notifyWaitingTest(UUID taskId) {
    var latch = waitLatches.remove(taskId);
    latch.countDown();
  }

}

最后一步是在一个测试用例中执行被测系统。我在这里使用带有JUnit 5的SpringBoot测试,但这对于使用Spring上下文的所有测试应该都一样。

@SpringBootTest
class ProductionIntegrationTest {

  @Autowired
  private Production sut;

  @Autowired
  private TaskCompletionEventListener listener;

  @Test
  void thatTaskCompletesSuccessfully() {
    var taskId = UUID.randomUUID();
    sut.doSomeTask(taskId);
    listener.waitForCompletion(taskId);
    // do some assertions like looking into the DB if value was stored successfully
  }

}

请注意,与此处的其他答案相反,如果您并行执行测试并且多个线程同时执行异步代码,则该解决方案也将起作用。


0

如果要测试逻辑,请不要异步进行测试。

例如,测试此代码可对异步方法的结果起作用。

public class Example {
    private Dependency dependency;

    public Example(Dependency dependency) {
        this.dependency = dependency;            
    }

    public CompletableFuture<String> someAsyncMethod(){
        return dependency.asyncMethod()
                .handle((r,ex) -> {
                    if(ex != null) {
                        return "got exception";
                    } else {
                        return r.toString();
                    }
                });
    }
}

public class Dependency {
    public CompletableFuture<Integer> asyncMethod() {
        // do some async stuff       
    }
}

在测试模拟中,同步实现具有依赖性。单元测试完全同步,运行时间为150ms。

public class DependencyTest {
    private Example sut;
    private Dependency dependency;

    public void setup() {
        dependency = Mockito.mock(Dependency.class);;
        sut = new Example(dependency);
    }

    @Test public void success() throws InterruptedException, ExecutionException {
        when(dependency.asyncMethod()).thenReturn(CompletableFuture.completedFuture(5));

        // When
        CompletableFuture<String> result = sut.someAsyncMethod();

        // Then
        assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
        String value = result.get();
        assertThat(value, is(equalTo("5")));
    }

    @Test public void failed() throws InterruptedException, ExecutionException {
        // Given
        CompletableFuture<Integer> c = new CompletableFuture<Integer>();
        c.completeExceptionally(new RuntimeException("failed"));
        when(dependency.asyncMethod()).thenReturn(c);

        // When
        CompletableFuture<String> result = sut.someAsyncMethod();

        // Then
        assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
        String value = result.get();
        assertThat(value, is(equalTo("got exception")));
    }
}

您无需测试异步行为,但可以测试逻辑是否正确。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.