Come utilizzare JUnit per testare i processi asincroni


204

Come testate i metodi che attivano i processi asincroni con JUnit?

Non so come far aspettare il mio test perché il processo finisca (non è esattamente un test unitario, è più simile a un test di integrazione in quanto coinvolge diverse classi e non solo una).


Potresti provare JAT (Java Asynchronous Test): bitbucket.org/csolar/jat
cs0lar

2
JAT ha 1 watcher e non è stato aggiornato da 1,5 anni. L'attendibilità è stata aggiornata appena 1 mese fa ed è alla versione 1.6 al momento della stesura di questo documento. Non sono affiliato con nessuno dei due progetti, ma se avessi intenzione di investire in un'aggiunta al mio progetto, darei più credibilità all'Awaitility in questo momento.
Les Hazlewood,

JAT non ha ancora aggiornamenti: "Ultimo aggiornamento 2013-01-19". Basta risparmiare tempo per seguire il link.
Deamon,

@LesHazlewood, un osservatore fa male a JAT, ma per anni nessun aggiornamento ... Solo un esempio. Con quale frequenza aggiorni lo stack TCP di basso livello del tuo sistema operativo, se funziona? All'alternativa a JAT viene fornita una risposta sotto stackoverflow.com/questions/631598/… .
user1742529

Risposte:


46

IMHO è una cattiva pratica fare in modo che i test unitari vengano creati o attesi sui thread, ecc. Si desidera che questi test vengano eseguiti in pochi secondi. Ecco perché mi piacerebbe proporre un approccio in due passaggi per testare i processi asincroni.

  1. Verifica che il tuo processo asincrono sia inviato correttamente. Puoi deridere l'oggetto che accetta le tue richieste asincrone e assicurarti che il lavoro inviato abbia proprietà corrette, ecc.
  2. Prova che i tuoi callback asincroni stanno facendo le cose giuste. Qui è possibile deridere il lavoro inviato in origine e supporre che sia inizializzato correttamente e verificare che i callback siano corretti.

148
Sicuro. Ma a volte è necessario testare il codice che dovrebbe specificamente gestire i thread.
Privo del

77
Per quelli di noi che usano Junit o TestNG per eseguire test di integrazione (e non solo test unitari) o test di accettazione dell'utente (ad es. W / Cucumber), è assolutamente necessario attendere un completamento asincrono e verificare il risultato.
Les Hazlewood,

38
I processi asincroni sono alcuni dei codici più complicati da ottenere e dici che non dovresti usare unit test per loro e testarli solo con un singolo thread? È una pessima idea.
Charles,

18
I test simulati spesso non riescono a dimostrare che le funzionalità funzionino end-to-end. La funzionalità asincrona deve essere testata in modo asincrono per assicurarsi che funzioni. Chiamalo test di integrazione se preferisci, ma è ancora un test necessario.
Scott Boring

4
Questa non dovrebbe essere la risposta accettata. I test vanno oltre i test unitari. L'OP lo definisce più un test di integrazione che un test unitario.
Jeremiah Adams,

191

Un'alternativa è utilizzare la classe 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());
    }
}

NOTA non è possibile utilizzare solo sincronizzato con un oggetto normale come blocco, poiché i callback rapidi possono rilasciare il blocco prima che venga chiamato il metodo di attesa del blocco. Vedi questo post sul blog di Joe Walnes.

EDIT Rimossi i blocchi sincronizzati attorno a CountDownLatch grazie ai commenti di @jtahlborn e @Ring


8
per favore non seguire questo esempio, non è corretto. si dovrebbe non essere la sincronizzazione su un CountDownLatch in quanto gestisce il thread-safe internamente.
jtahlborn,

1
È stato un buon consiglio fino alla parte sincronizzata, che ha mangiato probabilmente quasi 3-4 ore di tempo di debug. stackoverflow.com/questions/11007551/…
Anello del

2
Ci scusiamo per l'errore. Ho modificato la risposta in modo appropriato.
Martin,

7
Se stai verificando che onSuccess è stato chiamato, dovresti affermare che lock.await restituisce true.
Gilbert,

1
@Martin sarebbe corretto, ma significherebbe che hai un problema diverso che deve essere risolto.
George Aristy,

76

Puoi provare a utilizzare la libreria Awaitility . Rende facile testare i sistemi di cui stai parlando.


21
Un amichevole disclaimer: Johan è il principale collaboratore del progetto.
dbm

1
Soffre del problema fondamentale di dover aspettare (i test unitari devono essere eseguiti velocemente ). Idealmente non vuoi davvero aspettare un millisecondo più del necessario, quindi penso che usare CountDownLatch(vedi la risposta di @Martin) sia migliore in questo senso.
George Aristy,

Davvero fantastico.
R. Karlus,

Questa è la libreria perfetta che soddisfa i miei requisiti di test di integrazione del processo asincrono. Davvero fantastico. La libreria sembra essere ben mantenuta e ha funzionalità che vanno da quelle di base a quelle avanzate che credo siano sufficienti per soddisfare la maggior parte degli scenari. Grazie per il riferimento fantastico!
Tanvir,

Suggerimento davvero fantastico. Grazie
RoyalTiger

68

Se si utilizza CompletableFuture (introdotto in Java 8) o SettableFuture (da Google Guava ), è possibile terminare il test non appena viene eseguito, anziché attendere un periodo di tempo prestabilito. Il tuo test sarebbe simile a questo:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());

4
... e se sei bloccato con java meno di otto provare guava SettableFuture che fa praticamente la stessa cosa
Markus T


18

Un metodo che ho trovato molto utile per testare metodi asincroni è l'iniezione di un Executor un'istanza nel costruttore dell'oggetto da testare. In produzione, l'istanza di esecuzione è configurata per l'esecuzione in modo asincrono mentre in fase di test può essere simulata per l'esecuzione in modo sincrono.

Supponiamo quindi che sto provando a testare il metodo asincrono 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);
      }
    });
  }
}

In produzione, costruirei Foocon Executors.newSingleThreadExecutor()un'istanza di Executor mentre in test probabilmente lo costruirei con un esecutore sincrono che procede come segue:

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

Ora il mio test JUnit del metodo asincrono è abbastanza pulito -

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

Non funziona se voglio testare l'integrazione qualcosa che coinvolge una chiamata di libreria asincrona come Spring'sWebClient
Stefan Haberl

8

Non c'è nulla di intrinsecamente sbagliato nel testare il codice thread / asincrono, in particolare se il threading è il punto del codice che stai testando. L'approccio generale per testare queste cose è di:

  • Blocca il thread di prova principale
  • Cattura asserzioni non riuscite da altri thread
  • Sblocca il thread di prova principale
  • Ripeti eventuali fallimenti

Ma questo è un sacco di boilerplate per un test. Un approccio migliore / più semplice è usare solo ConcurrentUnit :

  final Waiter waiter = new Waiter();

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

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

Il vantaggio di questo rispetto CountdownLatchall'approccio è che è meno prolisso poiché gli errori di asserzione che si verificano in qualsiasi thread sono correttamente riportati al thread principale, il che significa che il test fallisce quando dovrebbe. Un writeup che confronta l' CountdownLatchapproccio a ConcurrentUnit è qui .

Ho anche scritto un post sul blog sull'argomento per coloro che vogliono imparare un po 'più di dettaglio.


una soluzione simile che ho usato in passato è github.com/MichaelTamm/junit-toolbox , anche come estensione di terze parti su junit.org/junit4
dschulten il

4

Che ne dici di chiamare SomeObject.waite notifyAllcome descritto qui O usando il metodo Robotiums Solo.waitForCondition(...) O usa una classe che ho scritto per fare questo (vedi commenti e prova classe per come usare)


1
Il problema con l'approccio wait / notification / interrupt è che il codice che stai testando può potenzialmente interferire con i thread in attesa (l'ho visto accadere). Questo è il motivo per cui ConcurrentUnit utilizza un circuito privato su cui i thread possono attendere, che non può essere inavvertitamente interferito da interruzioni al thread di test principale.
Jonathan,

3

Trovo una libreria socket.io per testare la logica asincrona. Sembra un modo semplice e breve usando LinkedBlockingQueue . Ecco un esempio :

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

Usando LinkedBlockingQueue prendi l'API per bloccare fino a ottenere risultati proprio come in modo sincrono. E imposta il timeout per evitare di assumere troppo tempo per attendere il risultato.


1
Approccio fantastico!
aminografia

3

Vale la pena ricordare che c'è un capitolo molto utile Testing Concurrent Programsin Concurrency in Practice che descrive alcuni approcci di unit test e offre soluzioni ai problemi.


1
Quale approccio è quello? Potresti fare un esempio?
Bruno Ferreira,

2

Questo è ciò che sto usando al giorno d'oggi se il risultato del test viene prodotto in modo asincrono.

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

Usando le importazioni statiche, il test è abbastanza carino. (nota, in questo esempio sto iniziando un thread per illustrare l'idea)

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

Se f.completenon viene chiamato, il test fallirà dopo un timeout. Puoi anche usare f.completeExceptionallyper fallire presto.


2

Ci sono molte risposte qui, ma una semplice è semplicemente creare un CompletableFuture completato e usarlo:

CompletableFuture.completedFuture("donzo")

Quindi nel mio test:

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

Mi sto solo assicurando che tutte queste cose vengano chiamate comunque. Questa tecnica funziona se stai usando questo codice:

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

Passerà attraverso di essa mentre tutti i CompletableFutures sono finiti!


2

Evita i test con thread paralleli ogni volta che puoi (che è il più delle volte). Questo renderà i tuoi test traballanti (a volte superano, a volte falliscono).

Solo quando devi chiamare qualche altra libreria / sistema, potresti dover aspettare su altri thread, in quel caso usa sempre la libreria Awaitility invece di Thread.sleep().

Non chiamare mai get()o join()nei test, altrimenti i test potrebbero essere eseguiti per sempre sul server CI nel caso in cui il futuro non venga mai completato. Affermare sempre isDone()prima nei test prima di chiamare get(). Per CompletionStage, cioè .toCompletableFuture().isDone().

Quando si verifica un metodo non bloccante come questo:

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

quindi non dovresti semplicemente testare il risultato passando un Future completato nel test, ma assicurati anche che il tuo metodo doSomething()non si blocchi chiamando join()oget() . Ciò è importante in particolare se si utilizza un framework non bloccante.

Per fare ciò, prova con un futuro non completato che hai impostato su completato manualmente:

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

In questo modo, se aggiungi future.join() a doSomething (), il test fallirà.

Se il tuo servizio utilizza un ExecutorService come in thenApplyAsync(..., executorService), quindi nei tuoi test iniettare un ExecutorService a thread singolo, come quello di guava:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Se il tuo codice utilizza forkJoinPool come thenApplyAsync(...) , riscrivi il codice per usare un ExecutorService (ci sono molti buoni motivi) o usa Awaitility.

Per abbreviare l'esempio, ho reso BarService un argomento di metodo implementato come lambda Java8 nel test, in genere sarebbe un riferimento iniettato che potresti deridere.


Ehi, @tkruse, forse hai un repository git pubblico con un test usando questa tecnica?
Cristiano,

@Christiano: sarebbe contrario alla filosofia SO. Invece, ho cambiato i metodi per compilare senza alcun codice aggiuntivo (tutte le importazioni sono java8 + o junit) quando li incolli in una classe di test junit vuota. Sentiti libero di votare.
tkruse,

Ho capito adesso. Grazie. Il mio problema ora è verificare quando i metodi restituiscono CompletableFuture ma accettano altri oggetti come parametri diversi da CompletableFuture.
Cristiano,

Nel tuo caso, chi crea il CompletableFuture che restituisce il metodo? Se è un altro servizio, può essere deriso e la mia tecnica è ancora valida. Se il metodo stesso crea un CompletableFuture, la situazione cambia molto in modo da poter fare una nuova domanda al riguardo. Dipende quindi da quale thread completerà il futuro restituito dal metodo.
tkruse,

1

Preferisco usare aspettare e avvisare. È semplice e chiaro

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

Fondamentalmente, dobbiamo creare un riferimento Array finale, da usare all'interno di una classe interna anonima. Preferirei creare un booleano [], perché posso mettere un valore per controllare se dobbiamo aspettare (). Al termine, rilasciamo semplicemente asyncExecuted.


1
Se la tua affermazione fallisce, il thread di test principale non lo saprà.
Jonathan,

Grazie per la soluzione, mi aiuta a eseguire il debug del codice con la connessione websocket.
Nazar Sakharenko,

@Jonathan, ho aggiornato il codice per rilevare eventuali asserzioni ed eccezioni e informarlo nel thread di test principale.
Paulo,

1

Per tutti gli utenti Spring là fuori, questo è il modo in cui di solito faccio i miei test di integrazione al giorno d'oggi, in cui è coinvolto il comportamento asincrono:

Attiva un evento dell'applicazione nel codice di produzione al termine di un'attività asincrona (come una chiamata I / O). Il più delle volte questo evento è comunque necessario per gestire la risposta dell'operazione asincrona in produzione.

Con questo evento attivo, puoi utilizzare la seguente strategia nel tuo caso di test:

  1. Eseguire il sistema in prova
  2. Ascolta l'evento e assicurati che l'evento sia stato attivato
  3. Fai le tue affermazioni

Per scomporlo, devi prima attivare un evento di dominio. Sto usando un UUID qui per identificare l'attività che è stata completata, ma ovviamente sei libero di usare qualcos'altro purché unico.

(Nota che i seguenti frammenti di codice usano anche le annotazioni Lombok per eliminare il codice della piastra della caldaia)

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

Il codice di produzione stesso si presenta quindi in questo modo:

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

}

Posso quindi utilizzare una primavera @EventListenerper catturare l'evento pubblicato nel codice di prova. Il listener di eventi è un po 'più coinvolto, perché deve gestire due casi in modo sicuro thread:

  1. Il codice di produzione è più veloce del test case e l'evento è già stato attivato prima che il test case controlli l'evento, oppure
  2. Il test case è più veloce del codice di produzione e il test case deve attendere l'evento.

A CountDownLatchviene utilizzato per il secondo caso, come indicato in altre risposte qui. Si noti inoltre che l' @Orderannotazione sul metodo del gestore eventi assicura che questo metodo del gestore eventi venga chiamato dopo qualsiasi altro listener di eventi utilizzato nella produzione.

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

}

L'ultimo passo è eseguire il sistema sotto test in un caso di test. Sto usando un test SpringBoot con JUnit 5 qui, ma questo dovrebbe funzionare allo stesso modo per tutti i test che utilizzano un contesto 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
  }

}

Nota che, contrariamente alle altre risposte qui, questa soluzione funzionerà anche se esegui i test in parallelo e più thread esercitano il codice asincrono contemporaneamente.


0

Se vuoi testare la logica, non provarla in modo asincrono.

Ad esempio per testare questo codice che funziona sui risultati di un metodo asincrono.

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

Nel test deridono la dipendenza con l'implementazione sincrona. Il test unitario è completamente sincrono e viene eseguito in 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")));
    }
}

Non testare il comportamento asincrono ma è possibile verificare se la logica è corretta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.