您如何测试使用JUnit触发异步过程的方法?
我不知道如何让我的测试等待流程结束(它不完全是单元测试,它更像是集成测试,因为它涉及多个类,而不仅仅是一个类)。
您如何测试使用JUnit触发异步过程的方法?
我不知道如何让我的测试等待流程结束(它不完全是单元测试,它更像是集成测试,因为它涉及多个类,而不仅仅是一个类)。
Answers:
恕我直言,让单元测试创建或在线程上等待是不好的做法。您希望这些测试能在几秒钟内运行。这就是为什么我想提出一种分两步的方法来测试异步过程。
一种替代方法是使用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周围的同步块
您可以尝试使用Awaitility库。它使测试您正在谈论的系统变得容易。
CountDownLatch
在这方面使用(参见@Martin的回答)更好。
如果使用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());
我发现一种对测试异步方法非常有用的方法是注入一个 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());
}
WebClient
测试线程/异步代码本质上没有错,特别是如果线程是您要测试的代码的重点。测试这些东西的一般方法是:
但这是一个测试的样板。更好/更简单的方法是只使用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是这里。
如何进行调用SomeObject.wait
,notifyAll
并按此处所述进行操作,或者使用Robotiums Solo.waitForCondition(...)
方法进行操作,或者使用我编写的类来做到这一点(有关用法,请参见注释和测试类)
我找到了一个库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进行阻塞,直到获得结果为止,就像同步方式一样。并设置超时以避免假设花费太多时间等待结果。
如果测试结果是异步产生的,这就是我现在正在使用的。
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
早期失败。
这里有很多答案,但是一个简单的答案就是创建一个完整的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完成时,它将在其中快速滑动!
尽可能避免使用并行线程进行测试(大多数情况下)。这只会使您的测试不稳定(有时会通过,有时会失败)。
仅在需要调用某些其他库/系统时,才可能需要等待其他线程,在这种情况下,请始终使用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的方法参数,通常它是您要模拟的注入引用。
我更喜欢使用等待和通知。简单明了。
@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。
对于那里的所有Spring用户,这是我如今通常进行集成测试的方式,其中涉及到异步行为:
异步任务(例如I / O调用)完成后,在生产代码中触发应用程序事件。无论如何,在大多数情况下,必须使用此事件来处理生产中异步操作的响应。
有了此事件,您就可以在测试案例中使用以下策略:
为了解决这个问题,您首先需要触发某种域事件。我在这里使用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
在测试代码中捕获已发布的事件。事件侦听器会涉及更多一点,因为它必须以线程安全的方式处理两种情况:
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
}
}
请注意,与此处的其他答案相反,如果您并行执行测试并且多个线程同时执行异步代码,则该解决方案也将起作用。
如果要测试逻辑,请不要异步进行测试。
例如,测试此代码可对异步方法的结果起作用。
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")));
}
}
您无需测试异步行为,但可以测试逻辑是否正确。