如何使用JUnit测试触发异步进程的方法?
我不知道如何让我的测试等待进程结束(它不是一个单元测试,它更像是一个集成测试,因为它涉及几个类而不仅仅是一个)。
答案 0 :(得分:167)
另一种方法是使用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撰写的this博文。
编辑删除了CountDownLatch周围的同步块,感谢@jtahlborn和@Ring的评论
答案 1 :(得分:62)
您可以尝试使用Awaitility库。它可以轻松测试您正在谈论的系统。
答案 2 :(得分:56)
如果您使用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());
答案 3 :(得分:40)
恕我直言,让单元测试创建或等待线程等是不好的做法。您希望这些测试在几秒钟内运行。这就是为什么我想提出一个两步测试异步过程的方法。
答案 4 :(得分:22)
启动此过程并使用Future
等待结果。
答案 5 :(得分:17)
我发现一种测试异步方法非常有用的方法是在object-to-test的构造函数中注入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
Executor实例构造Executors.newSingleThreadExecutor()
,而在测试中我可能会使用执行以下操作的同步执行器来构造它 -
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());
}
答案 6 :(得分:5)
测试线程/异步代码没有任何内在错误,特别是如果线程是您正在测试的代码的点。测试这些东西的一般方法是:
但这对于一次测试来说是很多样板。更好/更简单的方法是使用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进行比较的写入是here。
我还为那些想要了解更多细节的人写了一篇blog post。
答案 7 :(得分:4)
如所述here或使用 Robotiums SomeObject.wait
方法调用notifyAll
和Solo.waitForCondition(...)
或使用class i wrote进行操作这(请参阅注释和测试类以了解如何使用)
答案 8 :(得分:3)
值得一提的是Concurrency in Practice中有一个非常有用的章节Testing Concurrent Programs
,它描述了一些单元测试方法并为问题提供了解决方案。
答案 9 :(得分:2)
对于那里的所有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
,如此处其他答案所述。还要注意,事件处理程序方法上的@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
}
}
请注意,与此处的其他答案相反,如果您并行执行测试并且多个线程同时执行异步代码,则该解决方案也将起作用。
答案 10 :(得分: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(),测试将会失败。
如果您的服务使用诸如thenApplyAsync(..., executorService)
之类的ExecutorService,那么在您的测试中注入一个单线程ExecutorService,例如来自guava的那个:
ExecutorService executorService = Executors.newSingleThreadExecutor();
如果您的代码使用forkJoinPool,例如thenApplyAsync(...)
,请重写代码以使用ExecutorService(有很多好的理由),或者使用Awaitility。
为了缩短这个例子,我在测试中使BarService成为一个实现为Java8 lambda的方法参数,通常它是一个你要模拟的注入引用。
答案 11 :(得分: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完成后,它会直接压缩它!
答案 12 :(得分:1)
假设您有以下代码:
import React from "react";
import { Route, Redirect } from "react-router-dom";
import AdminLayout from "../layouts/Admin/Admin";
function PrivateRoute({ component: Component, roles, ...rest }) {
console.log("rest pvt route", ...rest);
return (
<Route
{...rest}
render={(props) => {
console.log("propsssss", props);
// if (!localStorage.getItem('userid')) {
if (!localStorage.getItem("access_token")) {
// not logged in so redirect to login page with the return url
return (
<Redirect
to={{ pathname: "/auth/login", state: { from: props.location } }}
/>
);
}
// logged in so return component
return <AdminLayout {...props} />;
}}
/>
);
}
export default { PrivateRoute };
尝试将其重构为如下形式:
public void method() {
CompletableFuture.runAsync(() -> {
//logic
//logic
//logic
//logic
});
}
然后,以这种方式测试subMethod:
public void refactoredMethod() {
CompletableFuture.runAsync(this::subMethod);
}
private void subMethod() {
//logic
//logic
//logic
//logic
}
这不是一个完美的解决方案,但是它将测试异步执行中的所有逻辑。
答案 13 :(得分:1)
我找到了一个库socket.io来测试异步逻辑。使用LinkedBlockingQueue看起来很简单。这是example:
@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来阻止,直到获得结果就像同步方式一样。并设置超时以避免假设有太多时间等待结果。
答案 14 :(得分:1)
如果测试结果异步生成,这就是我现在所使用的。
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
提前失败。
答案 15 :(得分:1)
我更喜欢使用wait并通知。它简单明了。
@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。
答案 16 :(得分:0)
JUnit 5具有Assertions.assertTimeout(Duration, Executable)
/ assertTimeoutPreemptively()
(请阅读每个Javadoc以了解它们之间的区别),而Mockito具有verify(mock, timeout(millisecs).times(x))
。
Assertions.assertTimeout(Duration.ofMillis(1000), () ->
myReactiveService.doSth().subscribe()
);
并且:
Mockito.verify(myReactiveService,
timeout(1000).times(0)).doSth(); // cannot use never() here
管道中的超时可能不确定/脆弱。所以要小心。
答案 17 :(得分: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")));
}
}
您不测试异步行为,但可以测试逻辑是否正确。