Подтвердить что ты не робот

Единичное тестирование андроидного приложения с модификацией и 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?

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

4b9b3361

Ответ 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