Unit testing android application with retrofit and

2019-01-17 17:03发布

问题:

I have developed an android app that is using retrofit with rxJava, and now I'm trying to set up the unit tests with Mockito but I don't know how to mock the api responses in order to create tests that do not do the real calls but have fake responses.

For instance, I want to test that the method syncGenres is working fine for my SplashPresenter. My classes are as follow:

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

the Api class is like:

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

}

Now I'm trying to test the SplashPresenterImpl class but I don't know how to do that, I should do something like:

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



}
}

Do you have any idea about how should I mock and verify the api responses? Thanks in advance!

EDIT: Following @invariant suggestion, now I'm passing a client object to my presenter, and that api returns an Observable instead of a Subscription. However, I'm getting a NullPointerException on my Subscriber when doing the api call. The test class looks like:

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

Why am I getting that NullPointerException?

Thanks a lot!

回答1:

How to test RxJava and Retrofit

1. Get rid of the static call - use dependency injection

The first problem in your code is that you use static methods. This is not a testable architecture, at least not easily, because it makes it harder to mock the implementation. To do things properly, instead of using Api that accesses ApiClient.getService(), inject this service to the presenter through the constructor:

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. Create the test class

Implement your JUnit test class and initialize the presenter with mock dependencies in the @Before method:

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. Mock and test

Then comes the actual mocking and testing, for example:

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

That way you can test your Observable + Subscription, if you want to test if the Observable behaves correctly, subscribe to it with an instance of TestSubscriber.


Troubleshooting

When testing with RxJava and RxAndroid schedulers, such as Schedulers.io() and AndroidSchedulers.mainThread() you might encounter several problems with running your observable/subscription tests.

NullPointerException

The first is NullPointerException thrown on the line that applies given scheduler, for example:

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

The cause is that AndroidSchedulers.mainThread() is internally a LooperScheduler that uses android's Looper thread. This dependency is not available on JUnit test environment, and thus the call results in a NullPointerException.

Race condition

The second problem is that if applied scheduler uses a separate worker thread to execute observable, the race condition occurs between the thread that executes the @Test method and the said worker thread. Usually it results in test method returning before observable execution finishes.

Solutions

Both of the said problems can be easily solved by supplying test-compliant schedulers, and there are few options:

  1. Use RxJavaHooks and RxAndroidPlugins API to override any call to Schedulers.? and AndroidSchedulers.?, forcing the Observable to use, for example, 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();
    }
    

    This code has to wrap the Observable test, so it can be done within @Before and @After as shown, it can be put into JUnit @Rule or placed anywhere in the code. Just don't forget to reset the hooks.

  2. Second option is to provide explicit Scheduler instances to classes (Presenters, DAOs) through dependency injection, and again just use Schedulers.immediate() (or other suitable for testing).

  3. As pointed out by @aleien, you can also use an injected RxTransformer instance that executes Scheduler application.

I've used the first method with good results in production.



回答2:

Make your syncGenres method return Observable instead of Subscription. Then you can mock this method to return Observable.just(...) instead of making a real api call.

If you'd like to keep Subscription as returned value in that method (which I don't advise as it breaks Observable composability) you'd need to make this method not static, and pass whatever ApiClient.getService() returns as a constructor parameter and use mocked service object in tests (this technique is called Dependency Injection)



回答3:

Is there any particular reason you return Subscription from your api methods? It is usually more handy to return Observable (or Single) from api methods (especially regarding to Retrofit being able to generate Observables and Singles instead of calls). If there is no special reason, I'd recommend to switch to something like this:

public interface Api {
    @GET("genres")
    Single<List<Genre>> syncGenres();
    ...
}

so your calls to api will look like:

...
Api api = retrofit.create(Api.class);
api.syncGenres()
   .subscribeOn(Schedulers.io())
   .observeOn(AndroidSheculers.mainThread())
   .subscribe(genres -> soStuff());

In that way, you'd be able to mock api class and write:

List<Genre> mockedGenres = Arrays.asList(genre1, genre2...);
Mockito.when(api.syncGenres()).thenReturn(Single.just(mockedGenres));

Also you'll have to consider that you won't be able to test responses on worker threads, since tests won't wait for them. For bypassing this problem I'd recommend reading these articles and consider using something like scheduler manager or transformer to be able to explicitly tell presenter which schedulers to use (real or test ones)



回答4:

I had the same problem with

.observeOn(AndroidSchedulers.mainThread())

i fixed it with the following codes

public class RxJavaUtils {
    public static Supplier<Scheduler> getSubscriberOn = () -> Schedulers.io();
    public static Supplier<Scheduler> getObserveOn = () -> AndroidSchedulers.mainThread();
}

and use it like this

deviceService.findDeviceByCode(text)
            .subscribeOn(RxJavaUtils.getSubscriberOn.get())
            .observeOn(RxJavaUtils.getObserveOn.get())

and in my test

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

works also for io.reactivex



回答5:

I use these classes:

  1. Service
  2. RemoteDataSource
  3. RemoteDataSourceTest
  4. TopicPresenter
  5. TopicPresenterTest

Simple Service:

public interface Service {
    String URL_BASE = "https://guessthebeach.herokuapp.com/api/";

    @GET("topics/")
    Observable<List<Topics>> getTopicsRx();

}

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

The key is MockWebServer from okhttp3.

This library makes it easy to test that your app Does The Right Thing when it makes HTTP and HTTPS calls. It lets you specify which responses to return and then verify that requests were made as expected.

Because it exercises your full HTTP stack, you can be confident that you’re testing everything. You can even copy & paste HTTP responses from your real web server to create representative test cases. Or test that your code survives in awkward-to-reproduce situations like 500 errors or slow-loading responses.

Use MockWebServer the same way that you use mocking frameworks like Mockito:

Script the mocks. Run application code. Verify that the expected requests were made. Here’s a complete example in 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();
    }

}

You can check my example in GitHub and this tutorial.

Also In the presenter you can see my server call with 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();
    }

}

And now 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());
    }

}

You can check my example in GitHub and this article.



回答6:

Solution be @maciekjanusz is perfect along with explanation, so I will only say this, the problem occurs when you use Schedulers.io() and AndroidSchedulers.mainThread(). The problem with @maciekjanusz's answer is that it's too complex to understand and still not everyone uses Dagger2 (which they should). Also, I'm not too sure but with RxJava2 my imports for RxJavaHooks wasn't working.

Better solution for RxJava2:-

Add RxSchedulersOverrideRule to your test package and just add the following line in your test class.

@Rule
public RxSchedulersOverrideRule schedulersOverrideRule = new RxSchedulersOverrideRule();

That's it, nothing else to add, your test cases should be running fine now.