Introduzione ad Awaitility

1. Introduzione

Un problema comune con i sistemi asincroni è che è difficile scrivere test leggibili per loro incentrati sulla logica aziendale e non contaminati da sincronizzazioni, timeout e controllo della concorrenza.

In questo articolo, daremo uno sguardo ad Awaitility, una libreria che fornisce un semplice linguaggio DSL (Domain-Specific Language) per il test di sistemi asincroni .

Con Awaitility, possiamo esprimere le nostre aspettative dal sistema in un DSL di facile lettura.

2. Dipendenze

Dobbiamo aggiungere le dipendenze di Awaitility al nostro pom.xml.

La libreria di attendibilità sarà sufficiente per la maggior parte dei casi d'uso. Nel caso in cui desideriamo utilizzare condizioni basate su proxy , dobbiamo anche fornire la libreria waitility-proxy :

 org.awaitility awaitility 3.0.0 test   org.awaitility awaitility-proxy 3.0.0 test 

È possibile trovare l'ultima versione delle librerie di attesa e attesa-proxy su Maven Central.

3. Creazione di un servizio asincrono

Scriviamo un semplice servizio asincrono e testiamolo:

public class AsyncService { private final int DELAY = 1000; private final int INIT_DELAY = 2000; private AtomicLong value = new AtomicLong(0); private Executor executor = Executors.newFixedThreadPool(4); private volatile boolean initialized = false; void initialize() { executor.execute(() -> { sleep(INIT_DELAY); initialized = true; }); } boolean isInitialized() { return initialized; } void addValue(long val) { throwIfNotInitialized(); executor.execute(() -> { sleep(DELAY); value.addAndGet(val); }); } public long getValue() { throwIfNotInitialized(); return value.longValue(); } private void sleep(int delay) { try { Thread.sleep(delay); } catch (InterruptedException e) { } } private void throwIfNotInitialized() { if (!initialized) { throw new IllegalStateException("Service is not initialized"); } } }

4. Testare con attesa

Ora creiamo la classe di test:

public class AsyncServiceLongRunningManualTest { private AsyncService asyncService; @Before public void setUp() { asyncService = new AsyncService(); } //... }

Il nostro test verifica se l'inizializzazione del nostro servizio avviene entro un periodo di timeout specificato (predefinito 10s) dopo aver chiamato il metodo di inizializzazione .

Questo test case attende semplicemente la modifica dello stato di inizializzazione del servizio o genera un'eccezione ConditionTimeoutException se la modifica dello stato non si verifica.

Lo stato è ottenuto da un Callable che interroga il nostro servizio a intervalli definiti (impostazione predefinita 100 ms) dopo un ritardo iniziale specificato (impostazione predefinita 100 ms). Qui stiamo usando le impostazioni predefinite per timeout, intervallo e ritardo:

asyncService.initialize(); await() .until(asyncService::isInitialized);

Qui, usiamo await , uno dei metodi statici della classe Awaitility . Restituisce un'istanza di una classe ConditionFactory . Possiamo anche usare altri metodi come quelli forniti per aumentare la leggibilità.

I parametri di temporizzazione predefiniti possono essere modificati utilizzando metodi statici della classe Awaitility :

Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS); Awaitility.setDefaultPollDelay(Duration.ZERO); Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);

Qui possiamo vedere l'uso della classe Duration , che fornisce costanti utili per i periodi di tempo utilizzati più di frequente.

Possiamo anche fornire valori di temporizzazione personalizzati per ciascuna chiamata in attesa . Qui ci aspettiamo che l'inizializzazione avvenga al massimo dopo cinque secondi e almeno dopo 100 ms con intervalli di polling di 100 ms:

asyncService.initialize(); await() .atLeast(Duration.ONE_HUNDRED_MILLISECONDS) .atMost(Duration.FIVE_SECONDS) .with() .pollInterval(Duration.ONE_HUNDRED_MILLISECONDS) .until(asyncService::isInitialized);

Vale la pena ricordare che ConditionFactory contiene metodi aggiuntivi come with , then , e , given. Questi metodi non fanno nulla e restituiscono solo questo , ma potrebbero essere utili per migliorare la leggibilità delle condizioni di test.

5. Utilizzo di Matchers

Awaitility consente anche l'uso di matcher hamcrest per controllare il risultato di un'espressione. Ad esempio, possiamo controllare che il nostro valore long sia cambiato come previsto dopo aver chiamato il metodo addValue :

asyncService.initialize(); await() .until(asyncService::isInitialized); long value = 5; asyncService.addValue(value); await() .until(asyncService::getValue, equalTo(value));

Si noti che in questo esempio, abbiamo utilizzato il primo attendono chiamata aspettare fino a quando il servizio viene inizializzato. In caso contrario, il metodo getValue genererebbe un'eccezione IllegalStateException .

6. Ignorare le eccezioni

A volte, abbiamo una situazione in cui un metodo genera un'eccezione prima che venga eseguito un lavoro asincrono. Nel nostro servizio, può essere una chiamata al metodo getValue prima che il servizio venga inizializzato.

Awaitility offre la possibilità di ignorare questa eccezione senza fallire un test.

Ad esempio, controlliamo che il risultato getValue sia uguale a zero subito dopo l'inizializzazione, ignorando IllegalStateException :

asyncService.initialize(); given().ignoreException(IllegalStateException.class) .await().atMost(Duration.FIVE_SECONDS) .atLeast(Duration.FIVE_HUNDRED_MILLISECONDS) .until(asyncService::getValue, equalTo(0L));

7. Utilizzo del proxy

As described in section 2, we need to include awaitility-proxy to use proxy-based conditions. The idea of proxying is to provide real method calls for conditions without implementation of a Callable or lambda expression.

Let's use the AwaitilityClassProxy.to static method to check that AsyncService is initialized:

asyncService.initialize(); await() .untilCall(to(asyncService).isInitialized(), equalTo(true));

8. Accessing Fields

Awaitility can even access private fields to perform assertions on them. In the following example, we can see another way to get the initialization status of our service:

asyncService.initialize(); await() .until(fieldIn(asyncService) .ofType(boolean.class) .andWithName("initialized"), equalTo(true));

9. Conclusion

In questo breve tutorial, abbiamo introdotto la libreria Awaitility, abbiamo familiarizzato con il suo DSL di base per il test di sistemi asincroni e abbiamo visto alcune funzionalità avanzate che rendono la libreria flessibile e facile da usare in progetti reali.

Come sempre, tutti gli esempi di codice sono disponibili su Github.