web-dev-qa-db-fra.com

Test unitaire des applications Android avec retrofit et rxjava

J'ai développé une application Android qui utilise retrofit avec rxJava, et maintenant j'essaye de configurer les tests unitaires avec Mockito mais je ne sais pas comment me moquer des réponses de l'API pour créer des tests qui ne font pas le vrai appels mais ont de fausses réponses.

Par exemple, je veux vérifier que la méthode syncGenres fonctionne correctement pour mon SplashPresenter. Mes cours sont les suivants:

public class SplashPresenterImpl implements SplashPresenter {

private SplashView splashView;

public SplashPresenterImpl(SplashView splashView) {
    this.splashView = splashView;
}

@Override
public void syncGenres() {
    Api.syncGenres(new Subscriber<List<Genre>>() {
        @Override
        public void onError(Throwable e) {
            if(splashView != null) {
                splashView.onError();
            }
        }

        @Override
        public void onNext(List<Genre> genres) {
            SharedPreferencesUtils.setGenres(genres);
            if(splashView != null) {
                splashView.navigateToHome();
            }
        }
    });
}
}

la classe Api est comme:

public class Api {
    ...
    public static Subscription syncGenres(Subscriber<List<Genre>> apiSubscriber) {
        final Observable<List<Genre>> call = ApiClient.getService().syncGenres();
        return call
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(apiSubscriber);
    }

}

Maintenant, j'essaie de tester la classe SplashPresenterImpl mais je ne sais pas comment faire ça, je devrais faire quelque chose comme:

public class SplashPresenterImplTest {

@Mock
Api api;
@Mock
private SplashView splashView;

@Captor
private ArgumentCaptor<Callback<List<Genre>>> cb;

private SplashPresenterImpl splashPresenter;

@Before
public void setupSplashPresenterTest() {
    // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
    // inject the mocks in the test the initMocks method needs to be called.
    MockitoAnnotations.initMocks(this);

    // Get a reference to the class under test
    splashPresenter = new SplashPresenterImpl(splashView);
}

@Test
public void syncGenres_success() {

    Mockito.when(api.syncGenres(Mockito.any(ApiSubscriber.class))).thenReturn(); // I don't know how to do that

    splashPresenter.syncGenres();
    Mockito.verify(api).syncGenres(Mockito.any(ApiSubscriber.class)); // I don't know how to do that



}
}

Avez-vous une idée de la façon dont je devrais me moquer et vérifier les réponses de l'API? Merci d'avance!

EDIT: Suivant la suggestion de @invariant, je passe maintenant un objet client à mon présentateur, et cette api renvoie un observable au lieu d’un abonnement. Cependant, je reçois une exception NullPointerException sur mon abonné lors de l'appel de l'API. La classe de test ressemble à:

public class SplashPresenterImplTest {
@Mock
Api api;
@Mock
private SplashView splashView;

private SplashPresenterImpl splashPresenter;

@Before
public void setupSplashPresenterTest() {
    // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
    // inject the mocks in the test the initMocks method needs to be called.
    MockitoAnnotations.initMocks(this);

    // Get a reference to the class under test
    splashPresenter = new SplashPresenterImpl(splashView, api);
}

@Test
public void syncGenres_success() {
    Mockito.when(api.syncGenres()).thenReturn(Observable.just(Collections.<Genre>emptyList()));


    splashPresenter.syncGenres();


    Mockito.verify(splashView).navigateToHome();
}
}

Pourquoi ai-je cette exception NullPointerException? 

Merci beaucoup!

15
FVod

Comment tester RxJava et Retrofit

1. Débarrassez-vous de l'appel statique - utilisez l'injection de dépendance

Le premier problème de votre code est que vous utilisez des méthodes statiques. Ce n'est pas une architecture testable, du moins difficilement, car il est plus difficile de se moquer de l'implémentation . Pour faire les choses correctement, au lieu d'utiliser Api qui accède à ApiClient.getService(), injectez ce service au présentateur via le constructeur:

public class SplashPresenterImpl implements SplashPresenter {

private SplashView splashView;
private final ApiService service;

public SplashPresenterImpl(SplashView splashView, ApiService service) {
    this.splashView = splashView;
    this.apiService = service;
}

2. Créer la classe de test

Implémentez votre classe de test JUnit et initialisez le présentateur avec des dépendances factices dans la méthode @Before:

public class SplashPresenterImplTest {

@Mock
ApiService apiService;

@Mock
SplashView splashView;

private SplashPresenter splashPresenter;

@Before
public void setUp() throws Exception {
    this.splashPresenter = new SplashPresenter(splashView, apiService);
}

3. Simulez et testez

Vient ensuite la moquerie et les tests, par exemple:

@Test
public void testEmptyListResponse() throws Exception {
    // given
    when(apiService.syncGenres()).thenReturn(Observable.just(Collections.emptyList());
    // when
    splashPresenter.syncGenres();
    // then
    verify(... // for example:, verify call to splashView.navigateToHome()
}

De cette façon, vous pouvez tester votre abonnement Observable +. Si vous souhaitez vérifier si l'Observable se comporte correctement, abonnez-vous avec une instance de TestSubscriber.


Dépannage

Lorsque vous testez avec les planificateurs RxJava et RxAndroid, tels que Schedulers.io() et AndroidSchedulers.mainThread(), vous risquez de rencontrer plusieurs problèmes lors de l'exécution de vos tests observables/d'abonnement. 

NullPointerException

Le premier est NullPointerException jeté sur la ligne qui applique le planificateur donné, par exemple:

.observeOn(AndroidSchedulers.mainThread()) // throws NPE

La cause en est que AndroidSchedulers.mainThread() est en interne un LooperScheduler qui utilise le thread Looper d'Android. Cette dépendance n'est pas disponible sur l'environnement de test JUnit et l'appel se traduit par une exception NullPointerException.

Condition de course

Le deuxième problème est que si le planificateur appliqué utilise un thread de travail distinct pour exécuter l'observable, la condition de concurrence se produit entre le thread qui exécute la méthode @Test et ledit thread de travail. Habituellement, il en résulte que la méthode de test revient avant la fin de l'exécution observable.

Solutions

Ces deux problèmes peuvent être facilement résolus en fournissant des ordonnanceurs conformes aux tests, et il existe peu d'options:

  1. Utilisez les API RxJavaHooks et RxAndroidPlugins pour remplacer tout appel à Schedulers.? et AndroidSchedulers.?, forçant l'observable à utiliser, par exemple, Scheduler.immediate():

    @Before
    public void setUp() throws Exception {
            // Override RxJava schedulers
            RxJavaHooks.setOnIOScheduler(new Func1<Scheduler, Scheduler>() {
                @Override
                public Scheduler call(Scheduler scheduler) {
                    return Schedulers.immediate();
                }
            });
    
            RxJavaHooks.setOnComputationScheduler(new Func1<Scheduler, Scheduler>() {
                @Override
                public Scheduler call(Scheduler scheduler) {
                    return Schedulers.immediate();
                }
            });
    
            RxJavaHooks.setOnNewThreadScheduler(new Func1<Scheduler, Scheduler>() {
                @Override
                public Scheduler call(Scheduler scheduler) {
                    return Schedulers.immediate();
                }
            });
    
            // Override RxAndroid schedulers
            final RxAndroidPlugins rxAndroidPlugins = RxAndroidPlugins.getInstance();
            rxAndroidPlugins.registerSchedulersHook(new RxAndroidSchedulersHook() {
                @Override
                public Scheduler getMainThreadScheduler() {
                    return Schedulers.immediate();
            }
        });
    }
    
    @After
    public void tearDown() throws Exception {
        RxJavaHooks.reset();
        RxAndroidPlugins.getInstance().reset();
    }
    

    Ce code doit encapsuler le test Observable. Il peut donc être exécuté dans @Before et @After comme indiqué, il peut être placé dans JUnit @Rule ou placé n'importe où dans le code. N'oubliez pas de réinitialiser les crochets.

  2. La deuxième option consiste à fournir des instances Scheduler explicites aux classes (Presenters, DAO) via une injection de dépendance, et à nouveau, il suffit d'utiliser Schedulers.immediate() (ou un autre moyen de test).

  3. Comme l'a souligné @aleien, vous pouvez également utiliser une instance injectée RxTransformer qui exécute l'application Scheduler

J'ai utilisé la première méthode avec de bons résultats en production.

28
maciekjanusz

Faites en sorte que votre méthode syncGenres retourne Observable au lieu de Subscription. Ensuite, vous pouvez vous moquer de cette méthode pour renvoyer Observable.just(...) au lieu de faire un véritable appel API.

Si vous souhaitez conserver Subscription en tant que valeur renvoyée dans cette méthode (ce que je ne conseille pas, car elle rompt la composabilité Observable), vous devez rendre cette méthode non statique et transmettre tout retour ApiClient.getService() en tant que paramètre de constructeur et en utiliser une fausse objet de service dans les tests (cette technique s'appelle l'injection de dépendance)

2
krp

Y a-t-il une raison particulière pour laquelle vous renvoyez un abonnement à partir de vos méthodes api? Il est généralement plus pratique de renvoyer des méthodes api Observable (ou Single) (notamment en ce qui concerne la possibilité pour Retrofit de générer des observables et des célibataires au lieu d’appels). S'il n'y a pas de raison particulière, je vous conseillerais de passer à quelque chose comme ceci:

public interface Api {
    @GET("genres")
    Single<List<Genre>> syncGenres();
    ...
}

donc vos appels à api ressembleront à:

...
Api api = retrofit.create(Api.class);
api.syncGenres()
   .subscribeOn(Schedulers.io())
   .observeOn(AndroidSheculers.mainThread())
   .subscribe(genres -> soStuff());

De cette façon, vous pourrez vous moquer de la classe api et écrire:

List<Genre> mockedGenres = Arrays.asList(genre1, genre2...);
Mockito.when(api.syncGenres()).thenReturn(Single.just(mockedGenres));

En outre, vous devrez tenir compte du fait que vous ne pourrez pas tester les réponses sur les threads de travail, car les tests ne les attendront pas. Pour contourner ce problème, je recommandela lecturecesarticles et envisage d'utiliser quelque chose comme gestionnaire de programmateur ou transformateur pour pouvoir indiquer explicitement au présentateur à quel programmateur utilisation (réelle ou test)

1
aleien

J'ai eu le même problème avec 

.observeOn (AndroidSchedulers.mainThread ())

je l'ai corrigé avec les codes suivants

public class RxJavaUtils {
    public static Supplier<Scheduler> getSubscriberOn = () -> Schedulers.io();
    public static Supplier<Scheduler> getObserveOn = () -> AndroidSchedulers.mainThread();
}

et l'utiliser comme ça

deviceService.findDeviceByCode(text)
            .subscribeOn(RxJavaUtils.getSubscriberOn.get())
            .observeOn(RxJavaUtils.getObserveOn.get())

et dans mon test 

@Before
public void init(){
    getSubscriberOn = () -> Schedulers.from(command -> command.run()); //Runs in curren thread
    getObserveOn = () -> Schedulers.from(command -> command.run()); //runs also in current thread
}

fonctionne aussi pour io.reactivex

0
Simon Ludwig

La solution be @maciekjanusz est parfaite avec une explication, aussi, le problème se produit lorsque vous utilisez Schedulers.io() et AndroidSchedulers.mainThread(). Le problème avec @ maciekjanusz est qu'il est trop complexe à comprendre et que tout le monde n'utilise pas encore Dagger2 (ce qu'ils devraient). En outre, je ne suis pas trop sûr mais avec RxJava2 mes importations pour RxJavaHooks ne fonctionnaient pas.

Meilleure solution pour RxJava2: -

Ajoutez RxSchedulersOverrideRule à votre package de test et ajoutez simplement la ligne suivante dans votre classe de test.

@Rule
public RxSchedulersOverrideRule schedulersOverrideRule = new RxSchedulersOverrideRule();

C'est ça, rien d'autre à ajouter, vos scénarios de test devraient bien fonctionner maintenant.

0
Rohan Kandwal

J'utilise ces cours:

  1. Un service
  2. RemoteDataSource
  3. RemoteDataSourceTest
  4. Sujet Présentateur
  5. TopicPresenterTest

Service simple:

public interface Service {
    String URL_BASE = "https://guessthebeach.herokuapp.com/api/";

    @GET("topics/")
    Observable<List<Topics>> getTopicsRx();

}

Pour RemoteDataSource

public class RemoteDataSource implements Service {

    private Service api;

    public RemoteDataSource(Retrofit retrofit) {


        this.api = retrofit.create(Service.class);
    }


    @Override
    public Observable<List<Topics>> getTopicsRx() {
        return api.getTopicsRx();
    }
}

La clé est MockWebServer de okhttp3 .

Cette bibliothèque vous permet de vérifier facilement que votre application agit comme il se doit lorsqu'elle appelle HTTP et HTTPS. Il vous permet de spécifier les réponses à renvoyer, puis de vérifier que les demandes ont été effectuées comme prévu.

Comme il exerce votre pile HTTP complète, vous pouvez être sûr de tout tester. Vous pouvez même copier-coller les réponses HTTP à partir de votre véritable serveur Web pour créer des scénarios de test représentatifs. Vous pouvez également vérifier que votre code survit dans des situations difficiles à reproduire, telles que 500 erreurs ou réponses à chargement lent.

Utilisez MockWebServer de la même manière que vous utilisez des frameworks moqueurs comme Mockito:

Script the mocks . Exécutez le code d'application . Vérifiez que les requêtes attendues ont été effectuées . Voici un exemple complet dans RemoteDataSourceTest:

public class RemoteDataSourceTest {

    List<Topics> mResultList;
    MockWebServer mMockWebServer;
    TestSubscriber<List<Topics>> mSubscriber;

    @Before
    public void setUp() {
        Topics topics = new Topics(1, "Discern The Beach");
        Topics topicsTwo = new Topics(2, "Discern The Football Player");
        mResultList = new ArrayList();
        mResultList.add(topics);
        mResultList.add(topicsTwo);

        mMockWebServer = new MockWebServer();
        mSubscriber = new TestSubscriber<>();
    }

    @Test
    public void serverCallWithError() {
        //Given
        String url = "dfdf/";
        mMockWebServer.enqueue(new MockResponse().setBody(new Gson().toJson(mResultList)));
        Retrofit retrofit = new Retrofit.Builder()
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(mMockWebServer.url(url))
                .build();
        RemoteDataSource remoteDataSource = new RemoteDataSource(retrofit);

        //When
        remoteDataSource.getTopicsRx().subscribe(mSubscriber);

        //Then
        mSubscriber.assertNoErrors();
        mSubscriber.assertCompleted();
    }

    @Test
    public void severCallWithSuccessful() {
        //Given
        String url = "https://guessthebeach.herokuapp.com/api/";
        mMockWebServer.enqueue(new MockResponse().setBody(new Gson().toJson(mResultList)));
        Retrofit retrofit = new Retrofit.Builder()
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(mMockWebServer.url(url))
                .build();
        RemoteDataSource remoteDataSource = new RemoteDataSource(retrofit);

        //When
        remoteDataSource.getTopicsRx().subscribe(mSubscriber);

        //Then
        mSubscriber.assertNoErrors();
        mSubscriber.assertCompleted();
    }

}

Vous pouvez consulter mon exemple dans GitHub et ce tutoriel .

Aussi dans le présentateur, vous pouvez voir mon appel de serveur avec RxJava:

public class TopicPresenter implements TopicContract.Presenter {

    @NonNull
    private TopicContract.View mView;

    @NonNull
    private BaseSchedulerProvider mSchedulerProvider;

    @NonNull
    private CompositeSubscription mSubscriptions;

    @NonNull
    private RemoteDataSource mRemoteDataSource;


    public TopicPresenter(@NonNull RemoteDataSource remoteDataSource, @NonNull TopicContract.View view, @NonNull BaseSchedulerProvider provider) {
        this.mRemoteDataSource = checkNotNull(remoteDataSource, "remoteDataSource");
        this.mView = checkNotNull(view, "view cannot be null!");
        this.mSchedulerProvider = checkNotNull(provider, "schedulerProvider cannot be null");

        mSubscriptions = new CompositeSubscription();

        mView.setPresenter(this);
    }

    @Override
    public void fetch() {

        Subscription subscription = mRemoteDataSource.getTopicsRx()
                .subscribeOn(mSchedulerProvider.computation())
                .observeOn(mSchedulerProvider.ui())
                .subscribe((List<Topics> listTopics) -> {
                            mView.setLoadingIndicator(false);
                            mView.showTopics(listTopics);
                        },
                        (Throwable error) -> {
                            try {
                                mView.showError();
                            } catch (Throwable t) {
                                throw new IllegalThreadStateException();
                            }

                        },
                        () -> {
                        });

        mSubscriptions.add(subscription);
    }

    @Override
    public void subscribe() {
        fetch();
    }

    @Override
    public void unSubscribe() {
        mSubscriptions.clear();
    }

}

Et maintenant, TopicPresenterTest:

@RunWith(MockitoJUnitRunner.class)
public class TopicPresenterTest {

    @Mock
    private RemoteDataSource mRemoteDataSource;

    @Mock
    private TopicContract.View mView;

    private BaseSchedulerProvider mSchedulerProvider;

    TopicPresenter mThemePresenter;

    List<Topics> mList;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);

        Topics topics = new Topics(1, "Discern The Beach");
        Topics topicsTwo = new Topics(2, "Discern The Football Player");
        mList = new ArrayList<>();
        mList.add(topics);
        mList.add(topicsTwo);

        mSchedulerProvider = new ImmediateSchedulerProvider();
        mThemePresenter = new TopicPresenter(mRemoteDataSource, mView, mSchedulerProvider);


    }

    @Test
    public void fetchData() {

        when(mRemoteDataSource.getTopicsRx())
                .thenReturn(rx.Observable.just(mList));

        mThemePresenter.fetch();

        InOrder inOrder = Mockito.inOrder(mView);
        inOrder.verify(mView).setLoadingIndicator(false);
        inOrder.verify(mView).showTopics(mList);

    }

    @Test
    public void fetchError() {

        when(mRemoteDataSource.getTopicsRx())
                .thenReturn(Observable.error(new Throwable("An error has occurred!")));
        mThemePresenter.fetch();

        InOrder inOrder = Mockito.inOrder(mView);
        inOrder.verify(mView).showError();
        verify(mView, never()).showTopics(anyList());
    }

}

Vous pouvez consulter mon exemple dans GitHub et cet article.

0
Cabezas