Модульное тестирование 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 метод и указанный рабочий поток. Обычно это приводит к возврату метода тестирования перед наблюдаемым выполнением заканчивает.

решения

обе указанные проблемы могут быть легко решены путем предоставления тест-совместимых планировщиков, и есть несколько вариантов:

  1. использовать 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 или помещается в любом месте кода. Только не забудь сбросить крючки.

  2. второй вариант - предоставить явное Scheduler экземпляры в классы (ведущие, DAOs) через инъекцию зависимостей, и снова просто используйте Schedulers.immediate() (или другое соответствующее для испытывать).

  3. как указал @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


я использую эти классы:

  1. сервис
  2. RemoteDataSource
  3. RemoteDataSourceTest
  4. TopicPresenter
  5. 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();

вот и все,больше нечего добавить, ваши тестовые случаи должны работать нормально.