Jak używać JUnit do testowania procesów asynchronicznych

204

Jak testujesz metody uruchamiające procesy asynchroniczne za pomocą JUnit?

Nie wiem, jak sprawić, by mój test czekał na zakończenie procesu (nie jest to dokładnie test jednostkowy, bardziej przypomina test integracyjny, ponieważ obejmuje kilka klas, a nie tylko jedną).

Sam
źródło
Możesz spróbować JAT (Java Asynchronous Test): bitbucket.org/csolar/jat
cs0lar,
2
JAT ma 1 obserwatora i nie był aktualizowany od 1,5 roku. Oczekiwanie zostało zaktualizowane zaledwie 1 miesiąc temu i jest w wersji 1.6 w momencie pisania tego tekstu. Nie jestem związany z żadnym projektem, ale jeśli miałbym zainwestować w dodatek do mojego projektu, dałbym teraz więcej wiary w Awaitility.
Les Hazlewood,
JAT wciąż nie ma aktualizacji: „Ostatnia aktualizacja 2013-01-19”. Po prostu zaoszczędź czas na skorzystanie z linku.
deamon
@LesHazlewood, jeden obserwator jest zły dla JAT, ale nie ma aktualizacji przez lata ... Tylko jeden przykład. Jak często aktualizujesz stos TCP niskiego poziomu w swoim systemie operacyjnym, jeśli to po prostu działa? Odpowiedź na alternatywę dla JAT znajduje się poniżej stackoverflow.com/questions/631598/… .
user1742529,

Odpowiedzi:

46

IMHO to zła praktyka, aby testy jednostkowe tworzyły wątki lub czekały na nich itp. Chcesz, aby testy te były uruchamiane w ułamkach sekund. Dlatego chciałbym zaproponować 2-etapowe podejście do testowania procesów asynchronicznych.

  1. Sprawdź, czy proces asynchroniczny został poprawnie przesłany. Możesz wyśmiewać obiekt, który akceptuje żądania asynchroniczne i upewnić się, że przesłane zadanie ma poprawne właściwości itp.
  2. Sprawdź, czy wywołania zwrotne asynchroniczne działają poprawnie. Tutaj możesz wykpić pierwotnie przesłane zadanie i założyć, że zostało ono poprawnie zainicjowane i sprawdzić, czy Twoje wywołania zwrotne są prawidłowe.
Cem Catikkas
źródło
148
Pewnie. Ale czasami trzeba przetestować kod specjalnie przeznaczony do zarządzania wątkami.
Lacker
77
Dla tych z nas, którzy używają Junit lub TestNG do przeprowadzania testów integracyjnych (a nie tylko testów jednostkowych) lub testów akceptacji użytkownika (np. Z ogórkiem), czekanie na zakończenie asynchroniczne i weryfikacja wyniku jest absolutnie konieczne.
Les Hazlewood,
38
Procesy asynchroniczne to jedne z najbardziej skomplikowanych kodów, które należy wykonać poprawnie. Mówisz, że nie powinieneś używać do nich testów jednostkowych i testować tylko z jednym wątkiem? To bardzo zły pomysł.
Charles
18
Fikcyjne testy często nie dowodzą, że funkcjonalność działa od początku do końca. Funkcję asynchroniczną należy przetestować w sposób asynchroniczny, aby upewnić się, że działa. Nazwij to testem integracyjnym, jeśli wolisz, ale jest to test, który jest nadal potrzebny.
Scott Boring,
4
To nie powinna być zaakceptowana odpowiedź. Testowanie wykracza poza testy jednostkowe. OP nazywa to bardziej testem integracyjnym niż testem jednostkowym.
Jeremiah Adams,
190

Alternatywą jest użycie klasy 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());
    }
}

UWAGA: nie można użyć synchronizacji ze zwykłym obiektem jako zamkiem, ponieważ szybkie wywołania zwrotne mogą zwolnić blokadę przed wywołaniem metody oczekiwania zamka. Zobacz ten post na blogu autorstwa Joe Walnesa.

EDYCJA Usunięto zsynchronizowane bloki wokół CountDownLatch dzięki komentarzom z @jtahlborn i @Ring

Jaskółka oknówka
źródło
8
nie podążaj za tym przykładem, jest niepoprawny. Państwo powinno nie być synchronizacji na CountDownLatch gdyż obsługuje wątku bezpieczeństwa wewnętrznie.
jtahlborn
1
To była dobra rada do momentu zsynchronizowania części, która pochłonęła prawdopodobnie około 3-4 godzin czasu debugowania. stackoverflow.com/questions/11007551/...
Zadzwoń
2
Przepraszamy za błąd. Odpowiednio zredagowałem odpowiedź.
Martin
7
Jeśli weryfikujesz, że onSuccess został wywołany, powinieneś potwierdzić, że lock.await zwraca true.
Gilbert
1
@Martin byłoby poprawne, ale oznaczałoby to, że masz inny problem, który należy naprawić.
George Aristy,
75

Możesz spróbować użyć biblioteki Awaitility . Ułatwia to testowanie systemów, o których mówisz.

Johan
źródło
21
Przyjazne wyłączenie odpowiedzialności: Johan jest głównym współtwórcą projektu.
dbm
1
Cierpi na podstawowy problem polegający na czekaniu (testy jednostkowe muszą przebiegać szybko ). Idealnie, naprawdę nie chcesz czekać milisekundy dłużej niż to konieczne, więc myślę, że użycie CountDownLatch(patrz odpowiedź @Martin) jest lepsze pod tym względem.
George Aristy
Naprawdę niesamowite.
R. Karlus,
To idealna biblioteka, która spełnia moje wymagania testu integracji procesu asynchronicznego. Naprawdę niesamowite. Biblioteka wydaje się być dobrze utrzymana i ma funkcje od podstawowego do zaawansowanego, które moim zdaniem są wystarczające do obsługi większości scenariuszy. Dzięki za niesamowite referencje!
Tanvir
Naprawdę niesamowita sugestia. Dzięki
RoyalTiger
68

Jeśli korzystasz z CompletableFuture (wprowadzony w Javie 8) lub SettableFuture (z Google Guava ), możesz zakończyć test, gdy tylko zostanie on ukończony, zamiast czekać na wcześniej ustalony czas. Twój test wyglądałby mniej więcej tak:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());
użytkownik393274
źródło
4
... a jeśli utkniesz z mniej niż ośmioma java, spróbuj guawy SettableFuture, która robi prawie to samo
Markus T.
23

Rozpocznij proces i poczekaj na wynik za pomocą Future.

Tom Hawtin - tackline
źródło
18

Jedną z metod, którą uważam za przydatną do testowania metod asynchronicznych, jest wstrzyknięcie Executor instancji do konstruktora obiekt-test. W środowisku produkcyjnym instancja modułu wykonującego jest skonfigurowana do działania asynchronicznego, podczas gdy w teście można ją wyśmiewać, aby działała synchronicznie.

Załóżmy, że próbuję przetestować metodę asynchroniczną 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);
      }
    });
  }
}

Podczas produkcji konstruowałbym Fooza pomocą Executors.newSingleThreadExecutor()instancji Executora, podczas gdy w testach prawdopodobnie konstruowałbym go za pomocą synchronicznego executora, który wykonuje następujące czynności -

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

Teraz mój test JUnit metody asynchronicznej jest całkiem czysty -

@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());
}
Matt
źródło
Nie działa, jeśli chcę przetestować integrację czegoś, co wymaga asynchronicznego wywołania biblioteki, takiego jak SpringWebClient
Stefan Haberl
8

Nie ma nic z natury błędnego w testowaniu kodu wątkowego / asynchronicznego, szczególnie jeśli wątek jest punktem testowanego kodu. Ogólne podejście do testowania tych rzeczy polega na:

  • Zablokuj główny wątek testowy
  • Przechwytywanie nieudanych twierdzeń z innych wątków
  • Odblokuj główny wątek testowy
  • Wycofaj wszelkie awarie

Ale to dużo płyty kotłowej na jeden test. Lepszym / prostszym podejściem jest po prostu użycie ConcurrentUnit :

  final Waiter waiter = new Waiter();

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

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

Zaletą tego CountdownLatchpodejścia jest to, że jest mniej gadatliwy, ponieważ awarie asercji występujące w dowolnym wątku są odpowiednio zgłaszane do głównego wątku, co oznacza, że ​​test kończy się niepowodzeniem, kiedy powinien. Zapis, który porównuje CountdownLatchpodejście do ConcurrentUnit jest tutaj .

Napisałem też post na blogu na ten temat dla tych, którzy chcą dowiedzieć się nieco więcej.

Jonathan
źródło
podobne rozwiązanie, z którego korzystałem w przeszłości, to github.com/MichaelTamm/junit-toolbox , również występujące jako rozszerzenie strony trzeciej na junit.org/junit4
dschulten
4

A może zadzwonisz SomeObject.waiti notifyAlljak opisano tutaj LUB przy użyciu metody Robotium Solo.waitForCondition(...) LUB LUB skorzystaj z klasy, którą napisałem, aby to zrobić (zobacz komentarze i klasy testowe, jak używać)

Dori
źródło
1
Problem z podejściem oczekiwania / powiadomienia / przerwania polega na tym, że testowany kod może potencjalnie zakłócać oczekujące wątki (widziałem, że tak się dzieje). Właśnie dlatego ConcurrentUnit używa prywatnego obwodu, na który wątki mogą czekać, na co nie można przypadkowo zakłócić przerwaniami głównego wątku testowego.
Jonathan
3

Znajduję bibliotekę socket.io do testowania logiki asynchronicznej. Wygląda to w prosty i krótki sposób za pomocą LinkedBlockingQueue . Oto przykład :

    @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();
}

Za pomocą LinkedBlockingQueue weź API do blokowania, aż uzyskasz wynik tak samo, jak w sposób synchroniczny. I ustaw limit czasu, aby uniknąć zakładania zbyt wiele czasu na oczekiwanie na wynik.

Fantasy Fang
źródło
1
Niesamowite podejście!
aminografia
3

Warto wspomnieć, że jest bardzo przydatny rozdział Testing Concurrent Programsw „ Współbieżności w praktyce”, który opisuje niektóre podejścia do testowania jednostkowego i podaje rozwiązania problemów.

jedenaście
źródło
1
Jakie to podejście? Czy możesz podać przykład?
Bruno Ferreira,
2

Tego właśnie używam obecnie, jeśli wynik testu jest generowany asynchronicznie.

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());
        }
    }
}

Przy użyciu importu statycznego test brzmi całkiem nieźle. (uwaga, w tym przykładzie zaczynam wątek ilustrujący pomysł)

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

Jeśli f.completenie zostanie wywołany, test zakończy się niepowodzeniem po upływie limitu czasu. Możesz także użyć f.completeExceptionallydo wczesnego niepowodzenia.

Jochen Bedersdorfer
źródło
2

Tutaj jest wiele odpowiedzi, ale najprostszym jest po prostu utworzenie kompletnej przyszłości i użycie jej:

CompletableFuture.completedFuture("donzo")

Więc w moim teście:

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

Po prostu upewniam się, że i tak wszystkie te rzeczy zostaną nazwane. Ta technika działa, jeśli używasz tego kodu:

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

Przebije się przez nią, gdy wszystkie CompletableFutures są gotowe!

markthegrea
źródło
2

Unikaj testowania z równoległymi wątkami, kiedy tylko możesz (co jest przez większość czasu). Spowoduje to jedynie, że twoje testy będą łuszczące się (czasem zdają, czasem kończą się niepowodzeniem).

Tylko wtedy, gdy musisz zadzwonić do innej biblioteki / systemu, być może będziesz musiał czekać na inne wątki, w takim przypadku zawsze używaj biblioteki Awaitility zamiast Thread.sleep().

Nigdy nie wywoływaj get()ani nie join()przeprowadzaj testów, w przeciwnym razie testy mogą działać wiecznie na serwerze CI, na wypadek gdyby przyszłość się nigdy nie zakończyła. Zawsze isDone()sprawdzaj w testach, zanim zadzwonisz get(). To znaczy dla CompletionStage .toCompletableFuture().isDone().

Podczas testowania takiej metody nieblokującej:

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

nie powinieneś po prostu testować wyniku, przekazując w teście ukończoną Przyszłość, powinieneś także upewnić się, że twoja metoda doSomething()nie blokuje się przez wywołaniejoin() lubget() . Jest to szczególnie ważne, jeśli używasz frameworka nieblokującego.

Aby to zrobić, przetestuj z niekompletną przyszłością, którą ustawiłeś na ukończenie ręcznie:

@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");
}

W ten sposób, jeśli dodasz future.join() do doSomething (), test się nie powiedzie.

Jeśli Twoja usługa korzysta z usługi ExecutorService, takiej jak in thenApplyAsync(..., executorService), wówczas w swoich testach wstrzyknij jedno-wątkową usługę ExecutorService, taką jak ta z guava:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Jeśli Twój kod korzysta z forkJoinPool, takiego jak thenApplyAsync(...) kod, aby użyć usługi ExecutorService (istnieje wiele dobrych powodów) lub użyj funkcji Oczekiwanie.

Aby skrócić ten przykład, uczyniłem BarService argumentem metody zaimplementowanym w teście jako lambda Java8, zwykle będzie to wstrzykiwane odniesienie, które wyśmiewałbyś.

tkruse
źródło
Hej @ tkruse, może masz publiczne repozytorium git z testem wykorzystującym tę technikę?
Cristiano
@Christiano: byłoby to sprzeczne z filozofią SO. Zamiast tego zmieniłem metody na kompilację bez dodatkowego kodu (wszystkie importowane pliki to java8 + lub junit) po wklejeniu ich do pustej klasy testowej junit. Zachęcamy do głosowania.
tkruse
Zrozumiałem teraz. dzięki. Moim problemem jest teraz sprawdzenie, kiedy metody zwracają CompletableFuture, ale akceptują inne obiekty jako parametry inne niż CompletableFuture.
Cristiano,
W twoim przypadku, kto tworzy CompletableFuture, który zwraca metoda? Jeśli jest to inna usługa, można ją wyśmiewać, a moja technika nadal obowiązuje. Jeśli sama metoda tworzy CompletableFuture, sytuacja bardzo się zmienia, więc możesz zadać nowe pytanie na jej temat. To zależy od tego, jaki wątek zakończy przyszłość, którą zwróci twoja metoda.
tkruse
1

Wolę używać czekaj i powiadamiaj. To jest proste i jasne.

@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];
    }
}

Zasadniczo musimy utworzyć ostateczne odwołanie do tablicy, które będzie używane wewnątrz anonimowej klasy wewnętrznej. Wolałbym stworzyć wartość logiczną [], ponieważ mogę ustawić wartość do kontroli, jeśli musimy czekać (). Kiedy wszystko jest zrobione, po prostu wypuszczamy asyncExecuted.

Paulo
źródło
1
Jeśli twoje twierdzenie się nie powiedzie, główny wątek testowy nie będzie o tym wiedział.
Jonathan
Dzięki za rozwiązanie, pomaga mi debugować kod z połączeniem WebSocket.
Nazar Sakharenko
@Jonathan, zaktualizowałem kod, aby przechwycić wszelkie stwierdzenia i wyjątki i poinformować go o głównym wątku testowym.
Paulo,
1

Dla wszystkich użytkowników Spring w ten sposób zwykle przeprowadzam obecnie testy integracyjne, w których występuje zachowanie asynchroniczne:

Uruchom zdarzenie aplikacji w kodzie produkcyjnym, gdy zakończy się zadanie asynchroniczne (takie jak wywołanie We / Wy). W większości przypadków to zdarzenie jest niezbędne do obsługi odpowiedzi operacji asynchronicznej podczas produkcji.

Po wdrożeniu tego zdarzenia możesz następnie użyć następującej strategii w przypadku testowym:

  1. Uruchom testowany system
  2. Nasłuchuj wydarzenia i upewnij się, że zostało ono uruchomione
  3. Czyń swoje twierdzenia

Aby rozwiązać ten problem, musisz najpierw uruchomić jakieś zdarzenie domeny. Korzystam z identyfikatora UUID, aby zidentyfikować ukończone zadanie, ale oczywiście możesz swobodnie korzystać z czegoś innego, o ile jest on unikalny.

(Uwaga: poniższe fragmenty kodu również wykorzystują adnotacje Lombok, aby pozbyć się kodu płyty kotła)

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

Sam kod produkcyjny zwykle wygląda następująco:

@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));
  }

}

Następnie mogę użyć sprężyny, @EventListeneraby złapać opublikowane zdarzenie w kodzie testowym. Nasłuchiwanie zdarzeń jest nieco bardziej zaangażowane, ponieważ musi obsługiwać dwa przypadki w sposób bezpieczny dla wątków:

  1. Kod produkcyjny jest szybszy niż przypadek testowy, a zdarzenie zostało już uruchomione, zanim przypadek testowy sprawdzi zdarzenie, lub
  2. Przypadek testowy jest szybszy niż kod produkcyjny i musi on czekać na zdarzenie.

A CountDownLatchjest używane w drugim przypadku, jak wspomniano w innych odpowiedziach tutaj. Należy również zauważyć, że @Orderadnotacja w metodzie obsługi zdarzeń zapewnia, że ​​ta metoda obsługi zdarzeń zostanie wywołana po każdym innym detektorze zdarzeń używanym w produkcji.

@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();
  }

}

Ostatnim krokiem jest wykonanie testowanego systemu w przypadku testowym. Używam tutaj testu SpringBoot z JUnit 5, ale powinien on działać tak samo dla wszystkich testów z kontekstem 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
  }

}

Zauważ, że w przeciwieństwie do innych odpowiedzi tutaj, to rozwiązanie będzie również działać, jeśli wykonasz testy równolegle, a wiele wątków jednocześnie wykona kod asynchroniczny.

Stefan Haberl
źródło
0

Jeśli chcesz przetestować logikę, nie testuj jej asynchronicznie.

Na przykład, aby przetestować ten kod, który działa na wynikach metody asynchronicznej.

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       
    }
}

W teście próbnym zależność z implementacją synchroniczną. Test jednostkowy jest całkowicie synchroniczny i trwa 150 ms.

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")));
    }
}

Nie testujesz zachowania asynchronicznego, ale możesz sprawdzić, czy logika jest poprawna.

Nils El-Himoud
źródło