Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android: Testing Reactive MVP Applications

Android: Testing Reactive MVP Applications

A strategy for testing the different layers of an MVP application on Android, with a focus on testing Rx Observables.

Richard Cirerol

August 04, 2016
Tweet

More Decks by Richard Cirerol

Other Decks in Technology

Transcript

  1. MVP (Model-View-Presenter) M. The stuff that isn't the view or

    the presenter V. The View P. The Presenter Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  2. MVP (Model-View-Presenter) Let's talk about them in this order instead

    V. The View P. The Presenter M. The stuff that isn't the view or the presenter Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  3. Building a Testable View Inject direct dependencies - Presenter -

    View altering libraries (Override Strings) Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  4. Building a Testable View Delegate actual work to the presenter

    Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  5. Building a Testable View Extract behavioral methods to an interface

    - Events for the presenter to trigger Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  6. public class LoginFragment implements LoginView { @Inject LoginPresenter presenter; @OnClick(R.id.loginButton)

    public void loginClick() { // Update visual state onLoginStarted() // Delegate the actual work to the presenter presenter.login(email.getText().toString(), password.getText().toString); } private void onLoginStarted(){ loginButton.setEnabled(false); errors.setVisiblity(View.GONE); progress.setVisiblity(View.VISIBLE); } } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  7. public interface LoginView { /** Updates visual state for successful

    login * * - hide wait state * - show success message * - start next activity */ void onLoginSuccess() /** Updates visual state for failed login * * - enable login button * - hide wait state * - show error based on reason object */ @Override public void onLoginFailed(LoginErrorReason reason); } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  8. Tes$ng the View Verify internal events: - Cause visual state

    changes - Make calls to the presenter Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  9. Tes$ng the View Verify external triggers: - Cause visual state

    changes Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  10. Tes$ng the View Limit the use of Reac/veX subscrip/ons Tes$ng

    Reac$ve MVP Applica$ons, Richard Cirerol
  11. Tes$ng the View • Espresso • Robolectric • Dagger •

    Mockito Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  12. Building a Testable Presenter Inject direct dependencies - Interactors -

    Services These are facades Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  13. Building a Testable Presenter No direct dependency on the Android

    framework. Don't pollute with android.* packages. Inject an interface instead. Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  14. @PerActivity public class LoginPresenter { private final LoginInteractor interactor; private

    CompositeSubscription subscriptions; private LoginView view; @Inject public LoginPresenter(LoginInteractor interactor){ this.interactor = interactor; } public void attach(LoginView view) { ... } public void detach() { ... } public void login(String email, String password) { ... } } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  15. // LoginPresenter public void login(String email, String password) { subscriptions.add(interactor.login(email,

    password) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<LoginStatus>() { public void onNext(LoginStatus status) { if (status.isSuccessful()) { view.onLoginSuccess(); } else { view.onLoginFailed(status.getReason()); } } public void onError(Throwable t) { view.onLoginFailed(LoginStatus.getReason(t)); } public void onCompleted() { } })); } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  16. Tes$ng the Presenter Minimum of 3 tests - Success -

    Error - Excep4on Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  17. Tes$ng the Presenter Each test will verify: - The correct

    method was called on the interactor - The view was updated Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  18. Tes$ng the Presenter Don't run anything on a background thread.

    Replace the schedulers. Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  19. @RunWith(MockitoJUnitRunner.class) public class LoginPresenterTest { @Mock LoginInteractor interactor; @Mock LoginView

    view; private LoginPresenter presenter; @Before public void setUp() throws Exception { RxAndroidPlugins.getInstance().reset(); RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() { @Override public Scheduler getMainThreadScheduler() { return Schedulers.immediate(); } }); presenter = new LoginPresenter(interactor); } @After public void tearDown() throws Exception { RxAndroidPlugins.getInstance().reset(); } } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  20. // LoginPresenterTest @Test public void testLoginSuccess() { when(interactor.login(anyString(), anyString())) .thenReturn(Observable.just(LoginStatus.SUCCESS));

    presenter.attach(view); presenter.login("email", "password"); verify(interactor, times(1)).login("email", "password"); verify(view, times(1)).onLoginSuccess(); verifyNoMoreInteractions(view); verifyNoMoreInteractions(interactor); } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  21. Building a Testable Model Same rules as Building a Testable

    Presenter - Inject dependencies - No direct dependency on the Android framework Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  22. @Singleton public class DefaultLoginInteractor implements LoginInteractor { private final UserService

    userService; private final UserService userManager; @Inject public DefaultLoginInteractor(UserService userService, UserService userManager){ this.userService = userService; this.userManager = userManager; } public Observable<LoginStatus> login(String email, String password) { ... } } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  23. @Singleton public class DefaultLoginInteractor implements LoginInteractor { ... public Observable<LoginStatus>

    login(String email, String password) { return userService.login(new DefaultIdentityRequest(emailUsername, password)) .subscribeOn(Schedulers.io()) .map(loginResponse -> { return loginResponse.isSuccessful() ? LoginStatus.SUCCESS : LoginStatus.fromResponse(loginResponse); }) .doOnNext(status -> { userManager.updateUser(status); }) .onErrorReturn(throwable -> { return LoginStatus.fromThrowable(throwable); }); } } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  24. Tes$ng the Interactor Similar to Tes$ng the Presenter • Mock

    dependencies • Use Rx Scheduler hooks to replace the current Schedulers • Test all paths (onNext success, onNext failure, onError, doOnNext, etc.) Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  25. Tes$ng the Interactor • Replace default schedulers with a TestScheduler

    • Subscribe to observables with a TestSubscriber Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  26. @RunWith(MockitoJUnitRunner.class) public class DefaultLoginInteractorTest { @Mock UserService service; @Mock UserManager

    userManager; private TestScheduler testScheduler; private DefaultLoginInteractor interactor; @Before public void setUp() throws Exception { testScheduler = Schedulers.test(); RxJavaHooks.reset(); RxJavaHooks.setOnIOScheduler(scheduler -> RxJavaTest.this.testScheduler); interactor = new DefaultLoginInteractor(userService, userManager); } @After public void tearDown() throws Exception { RxJavaHooks.reset(); } } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  27. // DefaultLoginInteractorTest @Test public void testLoginSuccess() { TestSubscriber subscriber =

    TestSubscriber.<LoginStatus>create(); when(userService.login(any(DefaultIdentityRequest.class))) .thenReturn(Observable.just(new LoginResponse("Success"))); interactor.login("email", "password").subscribe(subscriber); scheduler.triggerActions(); subscriber.awaitTerminalEvent(); subscriber.assertNoErrors(); subscriber.assertTerminalEvent(); LoginStatus status = subscriber.getOnNextEvents().get(0); assertThat(status).isEqualTo(LoginStatus.SUCCESS); verify(userService, times(1)).login(any(DefaultIdentityRequest.class)); verify(userManager, times(1)).updateUser(any(LoginStatus.class)); } Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  28. Tes$ng the Interactor • Always reset the hooks before the

    test (as well as a2er) • Prefer Schedulers.test() over Schedulers.immediate() • Can use Schedulers.immediate(), but YMMV • Don't use toBlocking() • If you use toBlocking(), make sure you add Fmeouts to your tests: @Test(timeout = 1000) Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol
  29. Tes$ng the Interactor • TestScheduler allows you to control the

    rate of the Observable stream. • TestSubscriber allows you to inspect a collec;on of items or errors from the Observable output Tes$ng Reac$ve MVP Applica$ons, Richard Cirerol