Модульное тестирование 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-интерфейс возвращает 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?

Большое спасибо!

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()
}

Таким образом, вы можете протестировать подписку Observable +, если вы хотите проверить, правильно ли работает Observable, подпишитесь на нее с экземпляром TestSubscriber,


Поиск проблемы

При тестировании с помощью планировщиков RxJava и RxAndroid, таких как Schedulers.io() а также AndroidSchedulers.mainThread() Вы можете столкнуться с несколькими проблемами при запуске ваших наблюдаемых тестов / тестов по подписке.

Исключение нулевого указателя

Первый NullPointerException выдается на строку, к которой применяется данный планировщик, например:

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

Причина в том, что AndroidSchedulers.mainThread() внутренне LooperScheduler который использует Android Looper нить. Эта зависимость недоступна в тестовой среде JUnit, и, следовательно, вызов приводит к исключению NullPointerException.

Состояние гонки

Вторая проблема заключается в том, что если применяемый планировщик использует отдельный рабочий поток для выполнения наблюдаемого, возникает состояние гонки между потоком, который выполняет @Test Способ и указанный рабочий поток. Обычно это приводит к возврату тестового метода до завершения наблюдаемого выполнения.

Решения

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

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

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

  3. Как отмечает @aleien, вы также можете использовать инъекцию RxTransformer экземпляр, который выполняет Scheduler приложение.

Я использовал первый метод с хорошими результатами в производстве.

Сделай свой syncGenres возврат метода Observable вместо Subscription, Тогда вы можете издеваться над этим методом, чтобы вернуть Observable.just(...) вместо того, чтобы сделать настоящий вызов API.

Если вы хотите сохранить Subscription как возвращаемое значение в этом методе (который я не советую, поскольку он ломается Observable сочетаемость) вам нужно сделать этот метод не статичным, и передать все ApiClient.getService() возвращает в качестве параметра конструктора и использует в тестах объект с поддельными сервисами (этот метод называется Dependency Injection)

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

  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 так же, как вы используете фреймворки 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();
    }

}

А теперь темаПрезентерТест:

@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 и в этой статье.

Есть ли какая-то особая причина, по которой вы возвращаете подписку из ваших методов 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));

Также вам следует учитывать, что вы не сможете тестировать ответы в рабочих потоках, так как тесты не будут их ждать. Чтобы обойти эту проблему, я бы порекомендовал прочитать эти статьи и рассмотреть возможность использования чего-то вроде диспетчера планировщика или преобразователя, чтобы иметь возможность явно указать докладчику, какие планировщики использовать (реальные или тестовые)

Решение будет @maciekjanusz идеально подходит вместе с объяснением, поэтому я скажу только это, проблема возникает, когда вы используете Schedulers.io() а также AndroidSchedulers.mainThread(), Проблема с ответом @ maciekjanusz состоит в том, что он слишком сложен для понимания, и до сих пор не все используют Dagger2 (что им и следует). Кроме того, я не слишком уверен, но с RxJava2 мой импорт для RxJavaHooks не работал

Лучшее решение для RxJava2:-

Добавьте RxSchedulersOverrideRule в свой тестовый пакет и просто добавьте следующую строку в свой тестовый класс.

@Rule
public RxSchedulersOverrideRule schedulersOverrideRule = new RxSchedulersOverrideRule();

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

У меня была такая же проблема с

.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
}

работает также для io.reactivex

Другие вопросы по тегам