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!
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;
}
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);
}
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
.
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.
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.
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.
Ces deux problèmes peuvent être facilement résolus en fournissant des ordonnanceurs conformes aux tests, et il existe peu d'options:
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.
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).
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.
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)
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)
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
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.
J'utilise ces cours:
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.