JUnit를 사용하여 비동기 프로세스를 테스트하는 방법
JUnit로 비동기 프로세스를 실행하는 방법을 어떻게 테스트하십니까?
그 과정이 끝나기를 기다리게 하는 방법(정확히 단위시험이 아니고, 한 과목만이 아니라 여러 과목에 관련되기 때문에 통합시험에 가깝다)을 모르겠다.
다른 방법은 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 주변에서 동기화된 블록 제거됨
Waitility 라이브러리를 사용해 보십시오.그것은 당신이 말하는 시스템을 쉽게 테스트할 수 있게 해준다.
Completable을 사용하는 경우미래(Java 8에 소개) 또는 설정 가능미래(구글구아바에서)는 미리 정해진 시간을 기다리지 않고 시험이 끝나는 대로 시험을 마칠 수 있다.네 테스트는 이렇게 보일 거야:
CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {
@Override
public void run() {
future.complete("Hello World!");
}
});
assertEquals("Hello World!", future.get());
IMHO 단위 테스트를 스레드 등에서 생성하거나 기다리는 것은 좋지 않은 관행이다.이 테스트는 몇 초 안에 실행되도록 하십시오.그렇기 때문에 비동기 프로세스 테스트에 대한 2단계 접근 방식을 제안하고자 한다.
- 비동기 프로세스가 제대로 제출되었는지 테스트하십시오.비동기 요청을 수락하는 개체를 조롱하고 제출된 작업에 올바른 속성 등이 있는지 확인할 수 있다.
- 비동기 콜백이 제대로 작동하는지 테스트하십시오.여기서 원래 제출된 작업을 조롱하고 올바르게 초기화되었다고 가정하고 콜백이 정확한지 확인할 수 있다.
프로세스를 시작하고 를 사용하여 결과를 기다리십시오.
하는데 꽤 중 이다.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());
}
특히 스레딩이 테스트 중인 코드의 포인트인 경우 스레딩/아스닉 코드를 테스트하는 데 본질적으로 문제가 있는 것은 아니다.이러한 제품을 테스트하는 일반적인 방법은 다음과 같다.
- 주 테스트 나사산 차단
- 다른 스레드로부터 실패한 주장 캡처
- 주 테스트 스레드 차단 해제
- 모든 고장 재투척도
하지만 그것은 한 번의 시험으로 엄청난 양의 보일러 판이다.더 나은/간단한 접근법은 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
로보튬을 사용한 수술방법에 따라 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를 통해 차단할 수 있다.그리고 결과를 기다리는 시간이 너무 길다고 가정하지 않도록 시간 초과를 설정하십시오.
JUnit 5는 다음과 같다.Assertions.assertTimeout(Duration, Executable)
/assertTimeoutPreemptively()
(차이를 이해하기 위해 각각의 자바도크를 읽어라)와 모키토는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
시간 제한은 파이프라인에서 비결정론적/취약적일 수 있다.그러니까 조심해.
매우 유용한 장이 있다는 것은 언급할 가치가 있다.Testing Concurrent Programs
일부 단위 시험 접근법을 설명하고 문제에 대한 해결책을 제공하는 실무에서의 동시성.
이것은 요즘 시험 결과가 비동기적으로 나온다면 내가 사용하고 있는 것이다.
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
일찍 실패하다
여기에 많은 답이 있지만 간단한 것은 완성된 Complete table을 만드는 것이다.미래 및 사용:
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();
모든 Complete table로 바로 연결될 것이다.선물은 끝났어!
가능하면 항상(대부분의 경우) 병렬 스레드로 테스트하지 마십시오.이것은 당신의 시험들을 삐걱거리게 할 뿐이다. (때로는 합격하고, 때로는 불합격한다.)
다른 라이브러리/시스템을 호출해야 할 때만 다른 스레드에서 대기해야 할 수 있으며, 이 경우 항상 다음 대신 Waitility 라이브러리를 사용하십시오.Thread.sleep()
.
전화만 하지 마라.get()
또는join()
테스트에서, 그렇지 않으면 미래에는 완료되지 않을 경우에 대비하여 CI 서버에서 영구적으로 테스트가 실행될 수 있다.항상 주장하다isDone()
전화하기 전 당신의 시험에서 첫 번째get()
즉. CompleteStage의 경우, 즉..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()
어떤 일을 하다()는 시험에 실패할 것이다.
서비스가 에서와 같은 실행자 서비스를 사용하는 경우thenApplyAsync(..., executorService)
그런 다음 테스트에서 단일 스레드 실행 서비스를 주입하십시오(예: guava:
ExecutorService executorService = Executors.newSingleThreadExecutor();
한다면, 를 들어 포크조인풀 을 사용한다면,thenApplyAsync(...)
, 실행자 서비스를 사용하기 위해 코드를 다시 쓰거나(여러 가지 타당한 이유가 있음) Waitility를 사용한다.
예를 들어, 나는 BarService를 테스트에서 Java8 람다로 구현된 메서드 인수로 만들었는데, 일반적으로 당신이 조롱하는 것은 주입된 참조가 될 것이다.
모든 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));
}
}
그러면 나는 봄을 사용할 수 있다.@EventListener
테스트 코드로 게시된 이벤트를 잡기 위해.이벤트 청취자는 다음과 같은 두 가지 사례를 안전하게 처리해야 하기 때문에 조금 더 관여하게 된다.
- 생산 코드는 테스트 케이스보다 빠르며 테스트 케이스가 이벤트를 확인하기 전에 이벤트가 이미 발생했거나,
- 테스트 케이스는 생산 코드보다 빠르며 테스트 케이스는 이벤트를 기다려야 한다.
A 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")));
}
}
비동기 동작을 테스트하지 않지만 논리가 올바른지 테스트할 수 있다.
나는 기다림과 알림을 사용하는 것을 선호한다.간단명료하다.
@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];
}
}
기본적으로 익명의 내부 계층 내부에서 사용할 최종 어레이 참조를 만들어야 한다.기다릴 필요가 있으면 통제할 수 있는 값을 넣을 수 있기 때문에 차라리 부울[]을 만들고 싶다.모든 작업이 완료되면 비동기 실행 파일을 해제하십시오.
다음 코드를 가지고 있다고 가정해 봅시다.
public void method() {
CompletableFuture.runAsync(() -> {
//logic
//logic
//logic
//logic
});
}
다음과 같은 방식으로 다시 작용하도록 하십시오.
public void refactoredMethod() {
CompletableFuture.runAsync(this::subMethod);
}
private void subMethod() {
//logic
//logic
//logic
//logic
}
그런 다음 다음과 같은 방법으로 하위 방법을 테스트하십시오.
org.powermock.reflect.Whitebox.invokeMethod(classInstance, "subMethod");
이것은 완벽한 해결책은 아니지만, 비동기 실행의 모든 논리를 테스트한다.
참조URL: https://stackoverflow.com/questions/631598/how-to-use-junit-to-test-asynchronous-processes
'programing' 카테고리의 다른 글
드롭다운 목록에서 확인란을 선택한 후 v-자동 완성(복수) 검색 입력을 지우는 방법 (0) | 2022.04.14 |
---|---|
Java: 날짜 생성자가 더 이상 사용되지 않는 이유와 내가 대신 사용하는 것은? (0) | 2022.04.13 |
Java에서 메서드를 비동기식으로 호출하는 방법 (0) | 2022.04.13 |
getter(Vuex)에서 상태를 호출할 때 정의되지 않음 (0) | 2022.04.13 |
Vue에서 계산된 속성/게터 디버링 (0) | 2022.04.13 |