Модульное тестирование android-приложения с дооснащением и rxjava
Я разработал android-приложение, которое использует дооснащение с rxJava, и теперь я пытаюсь настроить модульные тесты с Mockito, но я не знаю, как издеваться над ответами api, чтобы создать тесты, которые не делают реальных вызовов, но имеют поддельные ответы.
например, я хочу проверить, что метод syncGenres отлично работает для моего SplashPresenter. Мои занятия таковы:
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();
}
}
});
}
}
класс Api похож на:
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);
}
}
теперь я пытаюсь проверьте класс SplashPresenterImpl, но я не знаю, как это сделать, я должен сделать что-то вроде:
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
}
}
у вас есть идеи о том, как я должен издеваться и проверять ответы api? Заранее спасибо!
изменить: Следуя предложению @invariant, теперь я передаю объект клиента моему ведущему, и этот api возвращает наблюдаемый вместо подписки. Тем не менее, я получаю исключение NullPointerException на моем подписчике при выполнении вызова api. Тест класс например:
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();
}
}
Почему я получаю это исключение NullPointerException?
Спасибо большое!
6 ответов
как испытать RxJava и Retrofit
1. Избавьтесь от статического вызова - используйте инъекцию зависимостей
первая проблема в вашем коде, который вы используете статические методы. Это не проверяемая архитектура, по крайней мере, не легко, потому что это затрудняет издевательство над реализацией.
Чтобы делать все правильно, а не использовать Api
что обращается к ApiClient.getService()
, вводим сервис ведущему через конструктор:
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. Создайте тестовый класс
реализуйте свой тестовый класс JUnit и инициализируйте презентатора с фиктивными зависимостями в @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. Макет и испытаний
затем идет фактическое издевательство и тестирование, например:
@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()
}
таким образом, Вы можете проверить свою наблюдаемую подписку+, если вы хотите проверить, правильно ли ведет себя наблюдаемое, подпишитесь на него с экземпляром TestSubscriber
.
устранение неисправностей
при тестировании с планировщиками RxJava и RxAndroid, такими как Schedulers.io()
и AndroidSchedulers.mainThread()
при выполнении тестов observable / subscription может возникнуть несколько проблем.
NullPointerException
первое-это NullPointerException
бросается на строку, которая применяет данный планировщик, например:
.observeOn(AndroidSchedulers.mainThread()) // throws NPE
С AndroidSchedulers.mainThread()
внутренне a LooperScheduler
который использует android Looper
нить. Эта зависимость недоступна в тестовой среде JUnit, поэтому вызов приводит к исключению NullPointerException.
гонки
вторая проблема заключается в том, что если прикладной планировщик использует отдельный рабочий поток для выполнения observable, условие гонки возникает между потоком, который выполняет @Test
метод и указанный рабочий поток. Обычно это приводит к возврату метода тестирования перед наблюдаемым выполнением заканчивает.
решения
обе указанные проблемы могут быть легко решены путем предоставления тест-совместимых планировщиков, и есть несколько вариантов:
-
использовать
RxJavaHooks
иRxAndroidPlugins
API для переопределения любого вызоваSchedulers.?
иAndroidSchedulers.?
, заставляя наблюдаемое использовать, например,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(); }
этот код должен обернуть наблюдаемый тест, поэтому это можно сделать в
@Before
и@After
как показано, его можно положить в В JUnit@Rule
или помещается в любом месте кода. Только не забудь сбросить крючки. второй вариант - предоставить явное
Scheduler
экземпляры в классы (ведущие, DAOs) через инъекцию зависимостей, и снова просто используйтеSchedulers.immediate()
(или другое соответствующее для испытывать).как указал @aleien, вы также можете использовать injected
RxTransformer
экземпляра, который выполняетScheduler
приложение.
я использовал первый способ с хорошими результатами в производстве.
сделать свой syncGenres
метод return Observable
вместо Subscription
. Тогда вы можете издеваться над этим методом, чтобы вернуть Observable.just(...)
вместо того, чтобы делать реальный вызов api.
если вы хотите сохранить Subscription
как возвращаемое значение в этом методе (который я не советую, поскольку он ломается Observable
composability) вам нужно было бы сделать этот метод не статическим и передать все, что угодно ApiClient.getService()
возвращает в качестве параметра конструктора и использует насмешливый объект службы в тестах (этот метод называется инъекцией зависимостей)
есть ли какая-либо конкретная причина, по которой вы возвращаете подписку из своих методов api? Обычно удобнее возвращать Observable (или Single) из методов api (особенно в том, что касается дооснащения возможности генерировать Observables и Singles вместо вызовов). Если нет особой причины, я бы рекомендовал переключиться на что-то вроде этого:
public interface Api {
@GET("genres")
Single<List<Genre>> syncGenres();
...
}
поэтому ваши вызовы api будут выглядеть так:
...
Api api = retrofit.create(Api.class);
api.syncGenres()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSheculers.mainThread())
.subscribe(genres -> soStuff());
таким образом, Вы сможете издеваться над классом api и пиши:
List<Genre> mockedGenres = Arrays.asList(genre1, genre2...);
Mockito.when(api.syncGenres()).thenReturn(Single.just(mockedGenres));
Также вам придется учитывать, что вы не сможете тестировать ответы на рабочие потоки, так как тесты не будут ждать их. Для обхода этой проблемы я рекомендую чтение эти статьи и рассмотрите возможность использования чего-то вроде диспетчера планировщика или трансформатор чтобы иметь возможность явно указать презентатору, какие планировщики использовать (реальные или тестовые)
у меня была такая же проблема с
.observeOn (AndroidSchedulers.mainThread())
я исправил это со следующими кодами
public class RxJavaUtils {
public static Supplier<Scheduler> getSubscriberOn = () -> Schedulers.io();
public static Supplier<Scheduler> getObserveOn = () -> AndroidSchedulers.mainThread();
}
и использовать его в таком виде
deviceService.findDeviceByCode(text)
.subscribeOn(RxJavaUtils.getSubscriberOn.get())
.observeOn(RxJavaUtils.getObserveOn.get())
и в моем тесте
@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
}
строительство и для ввода-вывода.reactivex
я использую эти классы:
- сервис
- RemoteDataSource
- RemoteDataSourceTest
- TopicPresenter
- TopicPresenterTest
Услуги:
public interface Service {
String URL_BASE = "https://guessthebeach.herokuapp.com/api/";
@GET("topics/")
Observable<List<Topics>> getTopicsRx();
}
Для 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();
}
}
ключ MockWebServer от okhttp3.
эта библиотека позволяет легко проверить, что ваше приложение делает правильные вещи, когда он делает HTTP и HTTPS звонки. Он позволяет указать, какие ответы возвращать, а затем проверить, что запросы были сделаны так, как ожидалось.
поскольку он выполняет полный стек HTTP, вы можете быть уверены, что вы тестируете все. Вы даже можете копировать и вставлять HTTP-ответы с вашего реального веб-сервера для создания репрезентативных тестовых случаев. Или проверьте, что ваш код выживает в неудобных для воспроизведения ситуациях, таких как ошибки 500 или ответы с медленной загрузкой.
используйте MockWebServer так же, как вы используете mocking основы как Mockito:
сценарий глумится. Запустите код приложения. Убедитесь, что были сделаны ожидаемые запросы. Вот полный пример в 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();
}
}
вы можете проверить мой пример GitHub и в этом уроке.
также в презентаторе вы можете увидеть мой вызов сервера с 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();
}
}
а теперь 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());
}
}
вы можете проверить мой пример в GitHub и в данной статье.
решение be @maciekjanusz идеально подходит вместе с объяснением, поэтому я скажу только это, проблема возникает, когда вы используете Schedulers.io()
и AndroidSchedulers.mainThread()
. Проблема с ответом @maciekjanusz заключается в том, что он слишком сложен для понимания, и все еще не все используют Dagger2 (что они должны). Кроме того, я не слишком уверен, но с RxJava2 мой импорт для RxJavaHooks
не работает.
лучшее решение для RxJava2: -
добавить RxSchedulersOverrideRule в тестовый пакет и просто добавьте следующую строку в ваш тестовый класс.
@Rule
public RxSchedulersOverrideRule schedulersOverrideRule = new RxSchedulersOverrideRule();
вот и все,больше нечего добавить, ваши тестовые случаи должны работать нормально.