JUnit으로 비동기 프로세스를 시작하는 메소드를 어떻게 테스트합니까?
테스트가 프로세스가 끝날 때까지 기다리는 방법을 모르겠습니다 (정확히 단위 테스트가 아니라 하나의 클래스가 아닌 여러 클래스가 포함되므로 통합 테스트와 비슷합니다).
JUnit으로 비동기 프로세스를 시작하는 메소드를 어떻게 테스트합니까?
테스트가 프로세스가 끝날 때까지 기다리는 방법을 모르겠습니다 (정확히 단위 테스트가 아니라 하나의 클래스가 아닌 여러 클래스가 포함되므로 통합 테스트와 비슷합니다).
답변:
IMHO 스레드 등을 작성하거나 대기하는 단위 테스트를 수행하는 것은 좋지 않습니다. 이러한 테스트를 몇 초 내에 실행하려고합니다. 그렇기 때문에 비동기 프로세스 테스트에 2 단계 접근 방식을 제안하고자합니다.
대안은 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());
}
}
참고 잠금 대기 메소드가 호출되기 전에 빠른 콜백이 잠금을 해제 할 수 있으므로 일반 객체와 동기화 된 상태로만 사용할 수는 없습니다 . Joe Walnes 의이 블로그 게시물을 참조하십시오 .
EDIT @jtahlborn 및 @Ring의 의견 덕분에 CountDownLatch 주변의 동기화 된 블록이 제거되었습니다.
Awaitility 라이브러리를 사용해 볼 수 있습니다 . 말하는 시스템을 쉽게 테스트 할 수 있습니다.
CountDownLatch
(@Martin의 답변 참조)이 더 좋습니다.
당신이 사용하는 경우 CompletableFuture (자바 8에 도입) 또는 SettableFuture을 에서 ( 구글 구아바를 )가 아니라 시간의 미리 설정된 금액을 기다리는 것보다, 끝났어, 당신은 곧대로 시험 마무리를 할 수 있습니다. 테스트는 다음과 같습니다.
CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {
@Override
public void run() {
future.complete("Hello World!");
}
});
assertEquals("Hello World!", future.get());
프로세스를 시작하고를 사용하여 결과를 기다립니다 Future
.
비동기 메소드를 테스트하는 데 매우 유용한 방법 중 하나는 Executor
테스트 대상의 생성자에 인스턴스를 주입하는 것 입니다. 프로덕션에서 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()
시험에 아마 다음을 수행하는 동기 집행자로를 구성 할 때 집행자 인스턴스 -
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();
모든 CompletableFuture가 완료되면 바로 압축됩니다!
가능하면 병렬 스레드로 테스트하지 마십시오 (대부분의 경우). 이렇게하면 테스트가 벗겨지기 만합니다 (때로는 통과하고 때로는 실패합니다).
다른 라이브러리 / 시스템을 호출해야하는 경우에만 다른 스레드를 기다려야 할 수도 있습니다.이 경우 항상 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를 주입하십시오.
ExecutorService executorService = Executors.newSingleThreadExecutor();
코드에서와 같은 forkJoinPool thenApplyAsync(...)
을 사용하는 경우 코드를 다시 작성하여 ExecutorService (여러 가지 이유가 있음)를 사용하거나 Awaitility를 사용하십시오.
예제를 단축하기 위해 테스트에서 BarService를 Java8 람다로 구현 된 메소드 인수로 만들었습니다. 일반적으로 모의하는 참조가됩니다.
나는 기다림과 통보를 선호한다. 간단하고 명확합니다.
@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];
}
}
기본적으로 익명의 내부 클래스 내에서 사용하려면 최종 배열 참조를 만들어야합니다. wait ()해야 할 경우 제어 할 값을 넣을 수 있기 때문에 오히려 boolean []을 작성하려고합니다. 모든 것이 끝나면 asyncExecuted를 해제합니다.
모든 Spring 사용자의 경우 비동기 동작이 관련된 현재 통합 테스트를 수행하는 방법은 다음과 같습니다.
비동기 작업 (예 : I / O 호출)이 완료되면 프로덕션 코드에서 응용 프로그램 이벤트를 시작합니다. 어쨌든이 이벤트는 프로덕션에서 비동기 작업의 응답을 처리하는 데 필요합니다.
이 이벤트가 완료되면 테스트 케이스에서 다음 전략을 사용할 수 있습니다.
이 문제를 해결하려면 먼저 일종의 도메인 이벤트가 필요합니다. 완료된 작업을 식별하기 위해 여기에 UUID를 사용하고 있지만 고유 한 경우 다른 것을 자유롭게 사용할 수 있습니다.
(다음 코드 스 니펫은 롬복 주석을 사용 하여 보일러 플레이트 코드를 제거합니다.)
@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
}
}
여기의 다른 답변과 달리이 솔루션은 테스트를 병렬로 실행하고 여러 스레드가 동시에 비동기 코드를 행사하는 경우에도 작동합니다.
로직을 테스트하려면 비동기식으로 테스트하지 마십시오.
예를 들어 비동기 메소드의 결과에서 작동하는이 코드를 테스트합니다.
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")));
}
}
비동기 동작을 테스트하지는 않지만 논리가 올바른지 테스트 할 수 있습니다.