Testing di codice RxJava asincrono con Mockito

L’argomento del primo screencast che abbiamo fatto a fine Maggio era composto da alcune chiamate a un server REST effettuate utilizzando RxJava e Retrofit. Nel potete trovare le slide e il con la registrazione dell’evento. Abbiamo visto come combinare più chiamate asincrone gestendo in modo abbastanza semplice i thread su cui eseguire il tutto. Il codice risultante era molto semplice da leggere, in questo post partiremo da un esempio simile a quello visto nello screencast per vedere come fare a testare il codice asincrono scritto con RxJava.

Partiamo subito con un esempio: è una semplificazione rispetto a quello visto nello screencast, consiste in una chiamata al server di StackOverflow per ricavare i 5 top contributor. Dopo una prima chiamata che ritorna gli utenti, vengono selezionati solo i primi 5 e per ognuno viene effettuata una ulteriore chiamata al server per ricavare dei dati aggiuntivi:

service.getTopUsers()
    .flatMapIterable(UserResponse::getItems)
    .limit(5)
    .flatMap(this::loadUserStats)
    .toList();

Da notare che in questo caso abbiamo utilizzato il metodo flatMap, essendo le chiamate asincrone potrebbero esserci problemi di concorrenza e gli utenti potrebbero risultare non ordinati correttamente. L’obiettivo di questo post è proprio quello di scrivere un test automatico per verificare questo comportamento e correggere l’esempio in modo da far passare il test.
Il metodo loadUserStats permette di ricavare, attraverso una ulteriore chiamata REST, le informazioni sui badge ottenuti da un utente. Il codice completo dell’esempio è il seguente:

public class UserService {

    private StackOverflowService service;

    public UserService(StackOverflowService service) {
        this.service = service;
    }

    public Observable> loadUsers() {
        return service.getTopUsers()
            .flatMapIterable(UserResponse::getItems)
            .limit(5)
            .flatMap(this::loadUserStats)
            .toList();
    }

    private Observable loadUserStats(User user) {
        return service.getBadges(user.getId())
            .map(badges -> new UserStats(user, badges.getItems()));
    }
}

Da notare che l’oggetto gestito da Retrofit che permette di eseguire le chiamate REST è passato nel costruttore della classe e non è creato all’interno della classe stessa. Questo semplice esempio di dependency injection è fondamentale per poter effettuare il testing della classe in modo semplice.

Primo JUnit test con Mockito

Per iniziare con il testing possiamo scrivere un semplice JUnit test che sfrutta la libreria Mockito. L’oggetto da testare è una istanza di UserService, per testarlo in isolamento è necessario rompere la dipendenza verso l’oggetto di tipo StackOverflowService. Avendo usato la dependency injection il compito è semplice, possiamo costruire l’oggetto da testare passando un oggetto mock come dipendenza:

public class UserServiceTest {

    private StackOverflowService stackOverflowService = 
        Mockito.mock(StackOverflowService.class);

    private UserService userService;

    private TestObserver> testObserver = 
        new TestObserver<>();

    @Before
    public void setUp() {
        userService = new UserService(stackOverflowService);
    }

    @Test
    public void testSubscribe1() {
        userService.loadUsers().subscribe(testObserver);
    }
}

In questo modo possiamo simulare le chiamate al server REST decidendo di volta in volta su quali dati lavorare. L’oggetto di tipo TestObserver ci serve per poter verificare quali eventi sono generati dall’Observable, tornerà utile più avanti.
Utilizzando il test runner di Mockito è possibile riscrivere lo stesso test sfruttando anche le annotation Mock e InjectMocks:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock StackOverflowService service;

    @InjectMocks UserService userService;

    private TestObserver> testObserver = 
        new TestObserver<>();

    @Test
    public void testSubscribe() {
        userService.loadUsers().subscribe(testObserver);
    }
}

I due esempi sono equivalenti, l’annotation InjectMocks permette di popolare un campo creando un oggetto passando nel costruttore i mock definiti negli altri campi del test. In pratica in questo caso costruisce l’oggetto nello stesso modo in cui era costruito all’interno del metodo setUp dell’esempio precedente.
Il test è molto semplice e non ci aspettiamo niente di particolare, eseguendolo però si ha un NullPointerException non proprio banale da interpretare. Infatti non avendo definito il comportamento del mock viene ritornato il valore null da tutti i metodi. In particolare anche il metodo getTopUsers ritorna null causando una eccezione nell’invocazione del metodo flatMapIterable.
In questo caso può essere semplice trovare la causa dell’errore, in casi più complessi può essere utile cambiare il comportamento di default di Mockito. All’interno dell’annotation Mock è possibile cambiare il comportamento passando un valore dell’enum Answers di Mockito, nessuno dei valori presenti ci è però di aiuto (nel nostro caso vorremmo ritornare un Observable vuoto).
Possiamo sfruttare un’altra caratteristica di Mockito, aggiungendo una classe MockitoConfiguration all’interno del package org.mockito.configuration è possibile definire il comportamento di default da usare per tutti i metodi non riscritti esplicitamente. In questo caso controlliamo se il valore di ritorno del metodo invocato sul mock è un Observable e in caso positivo ritorniamo un Observable vuoto:

public class MockitoConfiguration extends DefaultMockitoConfiguration {
    public Answer getDefaultAnswer() {
        return new ReturnsEmptyValues() {
            @Override public Object answer(InvocationOnMock invocation) {
                Method method = invocation.getMethod();
                Class> returnType = method.getReturnType();
                if (returnType.isAssignableFrom(Observable.class)) {
                    return Observable.empty();
                }
                return super.answer(invocation);
            }
        };
    }
}

Eseguendo di nuovo il test non avremo più un NullPointerException e il test terminerà correttamente. Non avendo neanche una verifica non abbiamo testato ancora niente ma abbiamo un primo test funzionante da cui partire.

Test con Observable sincroni

Per poter testare il comportamento dell’oggetto è necessario definire i valori ritornati dal mock. Usando il metodo statico when di Mockito defininiamo i valori ritornati dai due metodi che saranno richiamati. Per iniziare facciamo ritonare due Observable molto semplici, il primo con un solo utente e il secondo con un solo badge:

when(service.getTopUsers())
    .thenReturn(just(
        new UserResponse(new User(1, "user 1"))));

when(service.getBadges(anyInt()))
    .thenReturn(just(
        new BadgeResponse(new Badge("badge"))));

Dopo aver richiamato il metodo subscribe è possibile verificare che l’Observable risultante ha emesso una lista con un solo valore seguito dall’evento di completamento. Per scrivere gli assert in modo semplice e mantenere il codice leggibile usiamo la libreria AssertJ al posto degli assert standard di JUnit. Il codice da usare in questo caso è il seguente:

assertThat(testObserver.getOnErrorEvents()).isEmpty();
assertThat(testObserver.getOnNextEvents()).hasSize(1);
assertThat(testObserver.getOnNextEvents().get(0)).hasSize(1);
assertThat(testObserver.getOnCompletedEvents()).hasSize(1);

Il codice completo del test rispecchia la struttura classica di un test spesso sintetizzata in Arrange-Act-Assert:

@Test
public void testSingleUser() {
    when(service.getTopUsers())
        .thenReturn(just(
            new UserResponse(new User(1, "user 1"))));

    when(service.getBadges(anyInt()))
        .thenReturn(just(
            new BadgeResponse(new Badge("badge"))));

    userService.loadUsers().subscribe(testObserver);

    assertThat(testObserver.getOnErrorEvents()).isEmpty();
    assertThat(testObserver.getOnNextEvents()).hasSize(1);
    assertThat(testObserver.getOnNextEvents().get(0)).hasSize(1);
    assertThat(testObserver.getOnCompletedEvents()).hasSize(1);
}

Eseguendo questo test tutto funziona correttamente, da notare che gli Observable ritornati sono sincroni e, per questo motivo, non ci sono problemi di concorrenza.
Continuiamo con i test complicando leggermente il caso da testare, vediamo a un esempio in cui sono ritornati due utenti:

when(service.getTopUsers())
    .thenReturn(just(
        new UserResponse(new User(1, "user 1"), new User(2, "user 2"))));

when(service.getBadges(anyInt()))
    .thenReturn(just(
        new BadgeResponse(new Badge("badge"))));

userService.loadUsers().subscribe(testObserver);

assertThat(testObserver.getOnErrorEvents()).isEmpty();
assertThat(testObserver.getOnNextEvents()).hasSize(1);
List users = testObserver.getOnNextEvents().get(0);
assertThat(users).extracting(UserStats::getId).containsExactly(1, 2);
assertThat(testObserver.getOnCompletedEvents()).hasSize(1);

Da notare che negli assert verifichiamo anche gli id degli oggetti emessi dall’Observable. Utilizzando il metodo extracting di AssertJ la lista di utenti viene trasformata in una lista di interi, successivamente usando il metodo containsExactly si controlla il contenuto della lista di interi.
Gli assert contenuti alla fine del test eseguono un controllo abbastanza standard quando si testa un Observable: viene controllato che non ci siano stati errori e che sia stato emesso un evento onNext e uno onCompleted. Per questo motivo è possibile estendere AssertJ per poter semplificare questo tipo di controllo, in questo caso il metodo che esegue il controllo è il seguente (maggiori informazioni su come creare assert custom sono disponibili nel sito ufficiale di AssertJ):

public TestObserverAssert hasEmittedOneValue(final Action1 checker) {
    Assertions.assertThat(actual.getOnErrorEvents()).isEmpty();
    Assertions.assertThat(actual.getOnCompletedEvents()).hasSize(1);
    Assertions.assertThat(actual.getOnNextEvents())
            .hasSize(1).has(new Condition() {
        @Override public boolean matches(T value) {
            checker.call(value);
            return true;
        }
    }, Index.atIndex(0));
    return this;
}

A questo punto il controllo alla fine del test può essere scritto usando un singolo assert:

assertThat(testObserver)
    .hasEmittedOneValue(
        users -> 
            assertThat(users)
                .extracting(UserStats::getId)
                .containsExactly(1, 2)
    );

Una cosa simile può essere fatta anche per semplificare il modo in cui viene definito il comportamento del mock, invece di usare una sintassi when().thenReturn(just(value)) sarebbe più corretto usare whenSubscribed().thenEmit(value). Per fare questo basta scrivere una semplice classe che internamente utiliza Mockito:

public class MockitoRx {
    public static  OngoingStubbingRx whenSubscribed(
            Observable methodCall) {
        return new OngoingStubbingRx<>(methodCall);
    }

    public static class OngoingStubbingRx {

        private OngoingStubbing> mock;

        public OngoingStubbingRx(Observable methodCall) {
            mock = Mockito.when(methodCall);
        }

        public OngoingStubbingRx thenEmit(T value) {
            mock = mock.thenReturn(just(value));
            return this;
        }

        public OngoingStubbingRx thenEmitException(Throwable t) {
            mock = mock.thenReturn(Observable.error(t));
            return this;
        }
    }
}

Usando i metodi (importati in modo statico) di questa classe il test si semplifica ulteriormente e diventa ancora più leggibile:

whenSubscribed(service.getTopUsers())
    .thenEmit(new UserResponse(new User(1, "user 1"), new User(2, "user 2")));

whenSubscribed(service.getBadges(anyInt()))
    .thenEmit(new BadgeResponse(new Badge("badge")));

userService.loadUsers().subscribe(testObserver);

assertThat(testObserver)
    .hasEmittedOneValue(
        users -> 
            assertThat(users)
                .extracting(UserStats::getId)
                .containsExactly(1, 2)
    );

Test delle chiamate asincrone

Un test più interessante è quello in cui si verifica il comportamento nel caso di concorrenza delle due chiamate per avere i dettagli dei due utenti; cosa succede nel caso in cui la prima chiamata dura più della seconda?
Per simulare questo caso sfruttiamo il metodo delay che permette di introdurre un ritardo nella generazione dei dati da parte dell’Observable su cui viene invocato:

when(service.getBadges(eq(1))).thenReturn(
    just(new BadgeResponse(new Badge("badge1")))
        .delay(2, TimeUnit.SECONDS));
when(service.getBadges(eq(2))).thenReturn(
    just(new BadgeResponse(new Badge("badge2")))
        .delay(1, TimeUnit.SECONDS));

In questo esempio la prima chiamata (identificata dal parametro passato valorizzato a 1) dura due secondi e, per questo motivo, terminerà dopo la seconda chiamata che dura solo un secondo. Controllando la documentazione del metodo delay si nota che l’esecuzione del metodo non è sincrona ma che viene utilizzato lo scheduler computation. Per questo motivo eseguendo il test si avrà un fallimento: l’assert viene verificato (sul thread principale) prima che il ritardo (su un thread separato) sia concluso.
Per evitare problemi è necessario usare uno scheduler creato a partire da un Executor in modo da poter aspettare l’emissione dei valori da parte dell’Observable. Il test completo è il seguente:

ExecutorService executor = Executors.newSingleThreadExecutor();

when(service.getTopUsers())
    .thenReturn(just(
        new UserResponse(new User(1, "user 1"), new User(2, "user 2"))));

when(service.getBadges(eq(1))).thenReturn(
    just(new BadgeResponse(new Badge("badge1")))
        .delay(2, TimeUnit.SECONDS));
when(service.getBadges(eq(2))).thenReturn(
    just(new BadgeResponse(new Badge("badge2")))
        .delay(1, TimeUnit.SECONDS));

userService.loadUsers()
    .observeOn(Schedulers.from(executor))
    .finallyDo(executor::shutdown)
    .subscribe(testObserver);

executor.awaitTermination(10, TimeUnit.SECONDS);

assertThat(testObserver)
    .hasEmittedOneValue(
        users -> 
            assertThat(users)
                .extracting(UserStats::getId)
                .containsExactly(1, 2)
    );

Eseguendo questo test AssertJ segnala un problema (con una descrizione molto chiara ma è sempre un errore!):

java.lang.AssertionError: 
Actual and expected have the same elements but not in the same order, at index 0 actual element was:
  <2>
whereas expected element was:
  <1>

Come ci saremmo aspettati c’è un problema di concorrenza legato all’utilizzo del metodo flatMap. Questa cosa può essere corretta facilmente, prima però è bene sottolineare un altro problema di questo test: la durata. Infatti usando il metodo delay nel modo appena visto sarà necessario aspettare realmente il ritardo. Test come questo non sono utili, uno unit test dovrebbe durare pochi millisecondi in modo da aver un feedback immediato anche nel caso di suite grandi.
Per risolvere questo problema è possibile usare un oggetto di tipo TestScheduler: in questo modo sarà possibile controllare l’andamento del tempo. Dopo aver creato un oggetto di tipo TestScheduler lo passiamo come terzo argomento del metodo delay:

when(service.getBadges(eq(1))).thenReturn(
    just(new BadgeResponse(new Badge("badge1")))
        .delay(2, TimeUnit.SECONDS, testScheduler));

Molti dei metodi invocabili su un Observable hanno una versione che prende anche uno scheduler, in questo modo è possibile decidere puntualmente per ogni metodo lo scheduler su cui sarà eseguito.
Per simulare l’avanzamento del tempo è possibile utilizzare il metodo advanceTimeBy, in questo caso simuliamo il trascorrere di due secondi:

testScheduler.advanceTimeBy(2, TimeUnit.SECONDS);

Anche in questo caso possiamo scrivere un metodo aggiuntivo nella classe MockitoRx (chiamato wait) per rendere il codice più leggibile. Il test completo è il seguente:

TestScheduler testScheduler = new TestScheduler();

whenSubscribed(service.getTopUsers())
    .thenEmit(new UserResponse(new User(1, "user 1"), new User(2, "user 2")));

whenSubscribed(service.getBadges(eq(1)))
    .wait(2, TimeUnit.SECONDS, testScheduler)
    .thenEmit(new BadgeResponse(new Badge("badge1")));
whenSubscribed(service.getBadges(eq(2)))
    .wait(1, TimeUnit.SECONDS, testScheduler)
    .thenEmit(new BadgeResponse(new Badge("badge2")));

userService.loadUsers().subscribe(testObserver);

testScheduler.advanceTimeBy(2, TimeUnit.SECONDS);

assertThat(testObserver)
    .hasEmittedOneValue(
        users -> 
            assertThat(users)
                .extracting(UserStats::getId)
                .containsExactly(1, 2)
    );

Eseguendo il test avremo la stessa segnalazione di errore del test precedente ma con il vantaggio di non dover aspettare realmente l’esecuzione del metodo delay.
Adesso che il test funziona correttamente correggiamo il codice della classe da testare, una soluzione è quella di sostituire la chiamata a flatMap con una a concatMap:

service.getTopUsers()
    .flatMapIterable(UserResponse::getItems)
    .limit(5)
    .concatMap(this::loadUserStats)
    .toList();

I due metodi sono simili, usando concatMap gli Observable ritornati dalla function vengono concatenati fra di loro e non eseguiti in parallelo togliendo quindi possibili problemi di concorrenza. Eseguendo il test avremo però un errore:

java.lang.AssertionError: 
Expected size:<1> but was:<0> in:
<[]>

Come detto le due chiamate da due secondi non sono eseguite in parallelo e quindi non è sufficiente aspettare solo due secondi. Cambiando il codice nel seguente modo il test viene eseguito correttamente:

testScheduler.advanceTimeBy(4, TimeUnit.SECONDS);

Una soluzione alternativa (suggerita da uno spettatore dello screencast, grazie!) consiste nel continuare a usare flatMap ma cambiare il modo in cui è creata la lista dei risultati. Usando il metodo toSortedList è possibile riordinare i risultati passando un Comparator:

service.getTopUsers()
    .flatMapIterable(UserResponse::getItems)
    .limit(5)
    .flatMap(this::loadUserStats)
    .toSortedList((u1, u2) -> 
        Integer.compare(u1.getReputation(), u2.getReputation()));

In questo modo il metodo funziona correttamente e le chiamate sono eseguite in parallelo!

Gestione degli errori con RxJava

Utilizzando Retrofit da una applicazione Android la possibilità che una chiamata REST non sia eseguita correttamente è abbastanza alta: la connessione di uno smartphone non sempre è affidabile. Per questo motivo è possibile gestire questi casi effettuando un ulteriore tentativo nel caso di errori prima di propagare all’esterno l’errore stesso. Avendo usato RxJava basta aggiungere una invocazione al metodo retry:

service.getTopUsers()
    .flatMapIterable(UserResponse::getItems)
    .limit(5)
    .flatMap(this::loadUserStats)
    .toSortedList((u1, u2) -> 
        Integer.compare(u1.getReputation(), u2.getReputation()));
    .retry(1);

Per simluare il caso di una chiamata REST che dà un errore momentaneo è possibile modificare il test:

whenSubscribed(service.getBadges(eq(1)))
    .thenEmitException(new IOException())
    .thenEmit(new BadgeResponse(new Badge("badge1")));

In questo modo alla prima invocazione del metodo getBadges con il parametro 1 sarà ritornato un Observable che emette una eccezione, alla successiva invocazione del metodo getBadges viene ritornando il valore corretto. Eseguendo il test con questa modifica è possibile verificare il comportamento del metodo retry.
Le cose si complicano se vogliamo testare un errore sul metodo getTopUsers, in questo caso il metodo viene invocato una sola volta. Infatti usando retry viene rieffettuata una nuova sottoscrizione all’Observable ritornato e non viene creato un nuovo Observable. Gli Observable creati da Retrofit sono cold, ogni volta che viene effettuata una sottoscrizione eseguono una chiamata REST verso il server.
Per testare questo caso è perciò necessario ritornare un Observable che alla prima sottoscrizione emette una eccezione e alla successiva il valore corretto:

when(service.getTopUsers())
    .thenReturn(Observable.create(new Observable.OnSubscribe() {
        boolean firstEmitted;

        @Override public void call(Subscriber super UserResponse> subscriber) {
            if (!firstEmitted) {
                subscriber.onError(new IOException());
                firstEmitted = true;
            } else {
                subscriber.onNext(
                    new UserResponse(new User(1, "user 1"), new User(2, "user 2")));
                subscriber.onCompleted();
            }
        }
    }));

Differenziare i due casi (in realtà c’è anche un terzo caso di un Observable che emette più di un valore) non è semplice, per questo motivo per adesso MockitoRx è una classe singola e non una libreria completa. Se pensate che sia comunque una buona idea (e magari volete dare una mano nello sviluppo!) lasciate un messaggio nei commenti di questo post!

Conclusioni

RxJava è una libreria molto potente, il codice da scrivere per le varie implementazioni è spesso costituito da poche righe ed è molto leggibile. Arrivare a quelle poche righe in molti casi non è banale, i metodi disponibili sono molti e non è sempre semplice capire quale utilizzare. A complicare ulteriormente la situazione c’è anche il fatto che gli Observable sono spesso asincroni (e di conseguenza il comportamento potrebbe variare in modo inaspettato). Per questi motivi è importante poter testare il codice usando degli unit test. L’obiettivo di questo post era proprio questo, nei commenti potete dirci se l’obiettivo è stato raggiunto!

 
 
Sviluppare applicazioni con React e Flux
 
 
prettyPre, formattare il tag PRE con jQuery e CSS

Fabio Collini

Software Architect con esperienza su piattaforma J2EE e attualmente focalizzato principalmente in progetti di sviluppo di applicazioni Android. Attualmente sono in Nana Bianca dove mi occupo dello sviluppo di alcune app Android. Coautore della seconda edizione di e docente di corsi di sviluppo su piattaforma Android. -