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

Ready to production - Testing your Android App

Ready to production - Testing your Android App

Presentation about testing approach for Android apps.

Presented at

- Androidos Day 2017
- Devfest Maceió 2017

Ubiratan Soares

September 23, 2017
Tweet

More Decks by Ubiratan Soares

Other Decks in Programming

Transcript

  1. ACTIVITY ( ~ 300 LoC) ADAPTER ( ~ 300 LoC)

    DATA ( ~ 50 LoC) ANOTHER DATA ( ~ 50 LoC)
  2. ACTIVITY ( ~ 300 LoC) MYADAPTER ( ~ 100 LoC)

    MYDATASOURCE ( ~ 50 LoC) BASE DATASOURCE ( ~ 50 LoC) BASEADAPTER ( ~ 250 LoC)
  3. Oferecem confiança para refactor, updates de bibliotecas, aplicar Proguard e

    outras operações que não impactam em alterações de regras de negócio 3
  4. UNIT TESTS (Mocked Contract) FUNCTIONAL UI TESTS INTEGRATION TESTS USECASE

    EXTERNAL WORLD ADAPTER PRESENTER VIEW CONTRACT PLATAFORM CONTROLLER SOURCE CONTRACT ENTITY INTEGRATION TESTS (DOUBLES) UNIT ACCEPTANCE E2E INTEGRATION UNIT TESTS (Mocked Contract + Mocked Usecase)
  5. VIEW PROTOCOL VIEW IMPL. PRESENTER FlowableSubscriber<U> Flowable<U> LifecycleStrategist Disposable DisposeStrategy

    LifecycleObserver LifecycleOwner (eg. Activity) DATA SOURCE INFRASTRUCTURE Flowable<T>
  6. /** * Created by bira on 6/29/17. * * Handles

    any errors throwed by GSON and report an * UnexpectedResponse to the foward steps * */ public class DeserializationIssuesHandler<T> implements FlowableTransformer<T, T> {
  7. public class DeserializationIssuesHandler<T> implements FlowableTransformer<T, T> { @Override public Publisher<T>

    apply(Flowable<T> upstream) { return upstream.onErrorResumeNext(this::handleErrorFromDeserializer); } private Publisher<T> handleErrorFromDeserializer(Throwable throwable) { if (isDeserializationError(throwable)) { return Flowable.error( new UnexpectedResponseError("Deserialization Error”) ); } return Flowable.error(throwable); } }
  8. public class DeserializationIssuesHandler<T> implements FlowableTransformer<T, T> { @Override public Publisher<T>

    apply(Flowable<T> upstream) { return upstream.onErrorResumeNext(this::handleErrorFromDeserializer); } private Publisher<T> handleErrorFromDeserializer(Throwable throwable) { if (isDeserializationError(throwable)) { return Flowable.error( new UnexpectedResponseError("Deserialization Error”) ); } return Flowable.error(throwable); } }
  9. public class DeserializationIssuesHandler<T> implements FlowableTransformer<T, T> { @Override public Publisher<T>

    apply(Flowable<T> upstream) { return upstream.onErrorResumeNext(this::handleErrorFromDeserializer); } private Publisher<T> handleErrorFromDeserializer(Throwable throwable) { if (isDeserializationError(throwable)) { return Flowable.error( new UnexpectedResponseError("Deserialization Error”) ); } return Flowable.error(throwable); } }
  10. private boolean isDeserializationError(Throwable throwable) { return throwable instanceof IllegalStateException ||

    throwable instanceof JsonIOException || throwable instanceof JsonSyntaxException || throwable instanceof JsonParseException; }
  11. @Test public void shouldHandle_GsonThrowsIllegalStateException() { Flowable<String> broken = Flowable.error(new IllegalStateException("Some

    message")); broken.compose(handler) .test() .assertError(this::checkHandledAsDeserializationError);
  12. @Test public void shouldHandle_GsonThrowsIllegalStateException() { Flowable<String> broken = Flowable.error(new IllegalStateException("Some

    message")); broken.compose(handler) .test() .assertError(this::checkHandledAsDeserializationError);
  13. @Test public void shouldHandle_GsonThrowsIllegalStateException() { Flowable<String> broken = Flowable.error(new IllegalStateException("Some

    message")); broken.compose(handler) .test() .assertError(this::checkHandledAsDeserializationError);
  14. @Test public void shouldHandle_GsonThrowsIllegalStateException() { Flowable<String> broken = Flowable.error(new IllegalStateException("Some

    message")); broken.compose(handler) .test() .assertError(this::checkHandledAsDeserializationError);
  15. DeserializationIssuesHandler<String> handler = new DeserializationIssuesHandler<>(); private boolean checkHandledAsDeserializationError(Throwable throwable) {

    if (throwable instanceof UnexpectedResponseError) { return throwable.getMessage().contentEquals("Deserialization Error"); } return false; }
  16. @Test public void shouldHandle_GsonThrowsIOException() { Flowable<String> broken = Flowable.error(new JsonIOException("Cannot

    read file")); broken.compose(handler) .test() .assertError(this::checkHandledAsDeserializationError); }
  17. @Test public void shouldNotHandle_OtherErrors() { IllegalAccessError ops = new IllegalAccessError(“OPS!");

    Flowable<String> broken = Flowable.error(ops); broken.compose(handler) .test() .assertError(throwable -> throwable.equals(ops)); }
  18. public class TriviaInfrastructure implements GetRandomFacts { // Constructor and fields

    ... @Override public Flowable<FactAboutNumber> fetchTrivia() { List<Integer> numbersForTrivia = triviaGenerator.numberForTrivia(); String formattedUrlPath = formatPathWithCommas(numbersForTrivia); return webService .getTrivia(formattedUrlPath) .subscribeOn(executionScheduler) .compose(networkingErrorHandler) .compose(restErrorsHandler) .compose(deserializationIssuesHandler) .filter(payload -> validator.accept(payload)) .map(payload -> mapper.toNumberFacts(payload)) .flatMap(Flowable::fromIterable); }
  19. public class TriviaInfrastructure implements GetRandomFacts { // Constructor and fields

    @Override public Flowable<FactAboutNumber> fetchTrivia() { List<Integer> numbersForTrivia = triviaGenerator.numberForTrivia(); String formattedUrlPath = formatPathWithCommas(numbersForTrivia); return webService .getTrivia(formattedUrlPath) .subscribeOn(executionScheduler) .compose(networkingErrorHandler) .compose(restErrorsHandler) .compose(deserializationIssuesHandler) .filter(payload -> validator.accept(payload)) .map(payload -> mapper.toNumberFacts(payload)) .flatMap(Flowable::fromIterable); }
  20. public class TriviaInfrastructureTests { TriviaInfrastructure infrastructure; MockWebServer server; @Before public

    void beforeEachTest() { server = new MockWebServer(); NumbersWebService numberAPI = new Retrofit.Builder() .baseUrl(server.url("/").toString()) .create(NumbersWebService.class); infrastructure = new TriviaInfrastructure( numberAPI, new TriviaGenerator(), new PayloadMapper(), new PayloadValidator(), Schedulers.trampoline() // non-concurrent integration on tests ); }
  21. public class TriviaInfrastructureTests { TriviaInfrastructure infrastructure; MockWebServer server; @Before public

    void beforeEachTest() { server = new MockWebServer(); NumbersWebService numberAPI = new Retrofit.Builder() .baseUrl(server.url("/").toString()) .create(NumbersWebService.class); infrastructure = new TriviaInfrastructure( numberAPI, new TriviaGenerator(), new PayloadMapper(), new PayloadValidator(), Schedulers.trampoline() // non-concurrent integration on tests ); }
  22. @Test public void checkIntegration_200OK_CorrectPayload() { String json = readFile("sample_response_200OK.json"); server.enqueue(

    new MockResponse() .setResponseCode(200) .setBody(json) ); infrastructure.fetchTrivia() .test() .assertNoErrors() .assertComplete() .assertValueCount(6); // Check at payload file }
  23. @Test public void checkIntegration_200OK_CorrectPayload() { String json = readFile("sample_response_200OK.json"); server.enqueue(

    new MockResponse() .setResponseCode(200) .setBody(json) ); infrastructure.fetchTrivia() .test() .assertNoErrors() .assertComplete() .assertValueCount(6); // Check at payload file }
  24. @Test public void checkIntegration_200OK_CorrectPayload() { String json = readFile("sample_response_200OK.json"); server.enqueue(

    new MockResponse() .setResponseCode(200) .setBody(json) ); infrastructure.fetchTrivia() .test() .assertNoErrors() .assertComplete() .assertValueCount(6); // Check at payload file }
  25. public class LoadingCoordination<T> implements FlowableTransformer<T, T> { private LoadingView view;

    private Scheduler uiScheduler; // Constructor @Override public Publisher<T> apply(Flowable<T> upstream) { ShowAtStartHideWhenDone<T> delegate = new ShowAtStartHideWhenDone<>( view.showLoading(), view.hideLoading(), uiScheduler ); return upstream.compose(delegate); } }
  26. public class LoadingCoordinationTests { Scheduler uiScheduler = Schedulers.trampoline(); LoadingCoordination<String> loadingCoordination;

    @Mock Action showAction; @Mock Action hideAction; @Before public void beforeEachTest() { MockitoAnnotations.initMocks(this); LoadingView view = new LoadingView() { @Override public Action showLoading() {return showAction;} @Override public Action hideLoading() {return hideAction;} }; loadingCoordination = new LoadingCoordination<>(view, uiScheduler); }
  27. public class LoadingCoordinationTests { Scheduler uiScheduler = Schedulers.trampoline(); LoadingCoordination<String> loadingCoordination;

    @Mock Action showAction; @Mock Action hideAction; @Before public void beforeEachTest() { MockitoAnnotations.initMocks(this); LoadingView view = new LoadingView() { @Override public Action showLoading() {return showAction;} @Override public Action hideLoading() {return hideAction;} }; loadingCoordination = new LoadingCoordination<>(view, uiScheduler); }
  28. @Test public void shouldCoordinateLoading_WhenFlowEmmits() throws Exception { Flowable.just("A", "B", "C")

    .compose(loadingCoordination) .subscribe(); checkLoadingCoordinated(); } @Test public void shouldCoordinateLoading_WithEmptyFlow() throws Exception { Flowable<String> empty = Flowable.empty(); empty.compose(loadingCoordination).subscribe(); checkLoadingCoordinated(); }
  29. private void checkLoadingCoordinated() throws Exception { InOrder inOrder = Mockito.inOrder(showAction,

    hideAction); inOrder.verify(showAction, oneTimeOnly()).run(); inOrder.verify(hideAction, oneTimeOnly()).run(); }
  30. private void checkLoadingCoordinated() throws Exception { InOrder inOrder = Mockito.inOrder(showAction,

    hideAction); inOrder.verify(showAction, oneTimeOnly()).run(); inOrder.verify(hideAction, oneTimeOnly()).run(); }
  31. private void checkLoadingCoordinated() throws Exception { InOrder inOrder = Mockito.inOrder(showAction,

    hideAction); inOrder.verify(showAction, oneTimeOnly()).run(); inOrder.verify(hideAction, oneTimeOnly()).run(); }
  32. @Test public void shouldPresent_NoContentError_IntoView() throws Exception { Flowable<FactAboutNumber> noContent =

    Flowable.error(new ContentNotFoundError()); when(usecase.fetchTrivia()).thenReturn(noContent); presenter.fetchRandomFacts(); BehavioursVerifier.with(view) .showLoadingFirstHideLoadingAfter() .shouldShowEmptyState() .shouldNotShowErrorState(); }
  33. @Test public void shouldPresent_NoContentError_IntoView() throws Exception { Flowable<FactAboutNumber> noContent =

    Flowable.error(new ContentNotFoundError()); when(usecase.fetchTrivia()).thenReturn(noContent); presenter.fetchRandomFacts(); BehavioursVerifier.with(view) .showLoadingFirstHideLoadingAfter() .shouldShowEmptyState() .shouldNotShowErrorState(); }
  34. public class BehavioursVerifier { private Object target; // Via factory

    method public BehavioursVerifier shouldShowErrorState() throws Exception { checkErrorStateView(); ErrorStateView view = (ErrorStateView) target; verify(view.showErrorState(), oneTimeOnly()).run(); return this; } // For each View and each behavior, check if bind / apply is possible private void checkEmptyStateView() { if (!(target instanceof EmptyStateView)) throw new IllegalArgumentException("Not an EmptyStateView"); }
  35. public class BehavioursVerifier { private Object target; public BehavioursVerifier shouldShowErrorState()

    throws Exception { checkErrorStateView(); ErrorStateView view = (ErrorStateView) target; verify(view.showErrorState(), oneTimeOnly()).run(); return this; } // For each View and each behavior, check if bind / apply is possible private void checkEmptyStateView() { if (!(target instanceof EmptyStateView)) throw new IllegalArgumentException("Not an EmptyStateView"); }
  36. public class BehavioursVerifier { private Object target; public BehavioursVerifier shouldShowErrorState()

    throws Exception { checkErrorStateView(); ErrorStateView view = (ErrorStateView) target; verify(view.showErrorState(), oneTimeOnly()).run(); return this; } // For each View and each behavior, check if bind / apply is possible private void checkEmptyStateView() { if (!(target instanceof EmptyStateView)) throw new IllegalArgumentException(“Not an EmptyStateView"); }
  37. @Test public void shouldPresent_AvailableData_IntoView() throws Exception { Flowable<FactAboutNumber> data =

    Flowable.just( FactAboutNumber.of("1", "1 is the first"), FactAboutNumber.of("2", "2 is the second") ); when(usecase.fetchTrivia()).thenReturn(data); presenter.fetchRandomFacts(); BehavioursRobot.with(view) .showLoadingFirstHideLoadingAfter() .disableRefreshFirstAndEnableAfter() .shouldNotShowEmptyState() .shouldNotShowErrorState() .shouldNotReportNetworkingError(); }
  38. @Test public void shouldPresent_AvailableData_IntoView() throws Exception { Flowable<FactAboutNumber> data =

    Flowable.just( FactAboutNumber.of("1", "1 is the first"), FactAboutNumber.of("2", "2 is the second") ); when(usecase.fetchTrivia()).thenReturn(data); presenter.fetchRandomFacts(); BehavioursRobot.with(view) .showLoadingFirstHideLoadingAfter() .disableRefreshFirstAndEnableAfter() .shouldNotShowEmptyState() .shouldNotShowErrorState() .shouldNotReportNetworkingError(); }
  39. @RunWith(RobolectricTestRunner.class) @Config( constants = BuildConfig.class, application = MainApplication.class, sdk =

    25 ) public class FactsViewModelMapperTests { FactsViewModelMapper mapper; @Before public void beforeEachTest() { Context context = RuntimeEnvironment.application; mapper = new FactsViewModelMapper(context); }
  40. @RunWith(RobolectricTestRunner.class) @Config( constants = BuildConfig.class, application = MainApplication.class, sdk =

    25 ) public class FactsViewModelMapperTests { FactsViewModelMapper mapper; @Before public void beforeEachTest() { Context context = RuntimeEnvironment.application; mapper = new FactsViewModelMapper(context); }
  41. @Test public void shouldMap_UpTo50Chars_AsTwoLabelsModel() { String text = "A text

    that should have 49 chars at most, no more”; FactAboutNumber fact = FactAboutNumber.of("1", text); FactViewModel model = mapper.translate(fact); assertThat(model).isInstanceOf(NumberAndFact.class); } @Test public void shouldMap_Above50Chars_AsSingleLabelModel() { String text = "Another text that is much,” + "much larger than 50 characters”; FactAboutNumber fact = FactAboutNumber.of("17", text); FactViewModel model = mapper.translate(fact); assertThat(model).isInstanceOf(ComposedWithSpannedStyles.class); }
  42. @Test public void shouldMap_UpTo50Chars_AsTwoLabelsModel() { String text = "A text

    that should have 49 chars at most, no more”; FactAboutNumber fact = FactAboutNumber.of("1", text); FactViewModel model = mapper.translate(fact); assertThat(model).isInstanceOf(NumberAndFact.class); } @Test public void shouldMap_Above50Chars_AsSingleLabelModel() { String text = "Another text that is much,” + "much larger than 50 characters”; FactAboutNumber fact = FactAboutNumber.of("17", text); FactViewModel model = mapper.translate(fact); assertThat(model).isInstanceOf(ComposedWithSpannedStyles.class); }
  43. @Test public void shouldMap_UpTo50Chars_AsTwoLabelsModel() { String text = "A text

    that should have 49 chars at most, no more”; FactAboutNumber fact = FactAboutNumber.of("1", text); FactViewModel model = mapper.translate(fact); assertThat(model).isInstanceOf(NumberAndFact.class); } @Test public void shouldMap_Above50Chars_AsSingleLabelModel() { String text = "Another text that is much,” + "much larger than 50 characters”; FactAboutNumber fact = FactAboutNumber.of("17", text); FactViewModel model = mapper.translate(fact); assertThat(model).isInstanceOf(ComposedWithSpannedStyles.class); }
  44. @RunWith(RobolectricTestRunner.class) @Config( constants = BuildConfig.class, application = MainApplication.class, sdk =

    25 ) public class FactsAboutNumbersActivityTest { FactsAboutNumbersActivity activity; @Before public void beforeEachTest() { activity = buildActivity(FactsAboutNumbersActivity.class) .create() .get(); }
  45. @RunWith(RobolectricTestRunner.class) @Config( constants = BuildConfig.class, application = MainApplication.class, sdk =

    25 ) public class FactsAboutNumbersActivityTest { FactsAboutNumbersActivity activity; @Before public void beforeEachTest() { activity = buildActivity(FactsAboutNumbersActivity.class) .create() .get(); }
  46. @Test public void shoulIntegrateActions_ForLoadingVisibility() throws Exception { ProgressBar loading =

    findById(activity, R.id.progressBar); activity.showLoading().run(); assertThat(loading.getVisibility()).isEqualTo(View.VISIBLE); activity.hideLoading().run(); assertThat(loading.getVisibility()).isEqualTo(View.GONE); }
  47. @Test public void shoulIntegrateActions_ForLoadingVisibility() throws Exception { ProgressBar loading =

    findById(activity, R.id.progressBar); activity.showLoading().run(); assertThat(loading.getVisibility()).isEqualTo(View.VISIBLE); activity.hideLoading().run(); assertThat(loading.getVisibility()).isEqualTo(View.GONE); }
  48. @Test public void shoulIntegrateAction_ForNetworkingErrorFeedback() throws Exception { activity.reportNetworkingError().run(); // From

    design_layout_snackbar_include.xml, since Snackbar // does not have an id assigned at his own container View snackText = findById(activity, R.id.snackbar_text); assertThat(snackText).isNotNull(); assertThat(snackText.getVisibility()).isEqualTo(View.VISIBLE); }
  49. @Test public void shoulIntegrateAction_ForNetworkingErrorFeedback() throws Exception { activity.reportNetworkingError().run(); // From

    design_layout_snackbar_include.xml, since Snackbar // does not have an id assigned at his own container View snackText = findById(activity, R.id.snackbar_text); assertThat(snackText).isNotNull(); assertThat(snackText.getVisibility()).isEqualTo(View.VISIBLE); }
  50. https://github.com/ubiratansoares/reactive-architectures-playground Sample usando numbersapi.com 100% RxJava2 + MVP, ilustrando conceitos

    vistos aqui Dagger 2.11, Full Android Support APIs Testes de unidade que importam Mais outras coisinhas (WIP)
  51. UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer, curious guy

    Google Developer Expert for Android Teacher, speaker, etc, etc