Единичное тестирование андроидного приложения с модификацией и 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?
Спасибо заранее!
EDIT:
Следуя @invariant предложение, теперь я передаю клиентский объект моему ведущему, и что api возвращает Observable вместо подписки. Тем не менее, я получаю исключение 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?
Спасибо большое!
Ответы
Ответ 1
Как протестировать 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 и инициализируйте презентацию с помощью mock-зависимостей в методе @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()
}
Таким образом, вы можете протестировать свою подписку Observable +, если вы хотите проверить правильность поведения Observable, подпишитесь на нее с экземпляром TestSubscriber
.
Устранение неполадок
При тестировании с помощью планировщиков RxJava и RxAndroid, таких как Schedulers.io()
и AndroidSchedulers.mainThread()
, вы можете столкнуться с несколькими проблемами при выполнении ваших тестов на проверку/подписку.
NullPointerException
Первая строка NullPointerException
, набрасываемая на строку, которая применяет данный планировщик, например:
.observeOn(AndroidSchedulers.mainThread()) // throws NPE
Причина в том, что AndroidSchedulers.mainThread()
является внутренним a LooperScheduler
, который использует поток android Looper
. Эта зависимость недоступна в тестовой среде JUnit, и, таким образом, вызов вызывает исключение NullPointerException.
Условие гонки
Вторая проблема заключается в том, что если прикладной планировщик использует отдельный рабочий поток для выполнения наблюдаемого, условие гонки происходит между потоком, который выполняет метод @Test
и указанный рабочий поток. Обычно это приводит к возврату метода тестирования до завершения наблюдения.
Решение
Обе эти проблемы могут быть легко решены путем предоставления тестовых совместимых планировщиков, и есть несколько вариантов:
-
Используйте API RxJavaHooks
и RxAndroidPlugins
для переопределения любого вызова Schedulers.?
и AndroidSchedulers.?
, заставляя Observable использовать, например, 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();
}
Этот код должен обернуть тест Observable, поэтому его можно сделать в @Before
и @After
, как показано на рисунке, его можно поместить в JUnit @Rule
или разместить в любом месте кода. Просто не забудьте reset крючки.
-
Второй вариант - предоставить явные Scheduler
экземпляры классам (презентаторам, DAO) через инъекцию зависимостей и снова просто использовать Schedulers.immediate()
(или другое подходящее для тестирования).
-
Как указано в @aleien, вы также можете использовать инъецированный экземпляр RxTransformer
, который выполняет приложение Scheduler
.
Я использовал первый метод с хорошими результатами в производстве.
Ответ 2
Сделайте ваш метод syncGenres
возвратите Observable
вместо Subscription
. Затем вы можете издеваться над этим методом, чтобы вернуть Observable.just(...)
вместо создания реального вызова api.
Если вы хотите сохранить Subscription
в качестве возвращаемого значения в этом методе (который я не советую, так как он разрушает Observable
), вам нужно будет сделать этот метод не статическим и передать все ApiClient.getService()
возвращает в качестве параметра конструктора и использует обманутый объект службы в тестах (этот метод называется Injection Dependency)
Ответ 3
Есть ли какая-то особая причина, по которой вы возвращаете подписку из ваших методов api? Обычно удобнее возвращать Observable (или Single) из api-методов (особенно в том случае, если Retrofit способен генерировать 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));
Также вам придется подумать, что вы не сможете тестировать ответы на рабочие потоки, так как тесты не будут ждать их. Для обхода этой проблемы я бы рекомендовать чтение эти статьи и подумайте над тем, чтобы использовать что-то вроде диспетчера планировщика или , чтобы иметь возможность явно указывать ведущему, какие планировщики использовать (реальные или тестовые)
Ответ 4
Я использую эти классы:
- Сервис
- 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 так же, как вы используете насмешливые фреймворки, такие как Mockito:
Script насмешки.
Запустите код приложения.
Убедитесь, что ожидаемые запросы были сделаны.
Вот полный пример в 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 и this учебник.
Также в презентаторе вы можете увидеть мой серверный вызов с помощью 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 и this статья.
Ответ 5
У меня была та же проблема с
.observeOn(AndroidSchedulers.mainThread())
i зафиксировал его следующими кодами
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
}
работает также для io.reactivex