Slide 1

Slide 1 text

READY TO PRODUCTION TESTANDO SEU APP ANDROID Ubiratan Soares Agosto / 2017

Slide 2

Slide 2 text

COMO VOCÊ SABE QUE O PRODUTO DE SOFTWARE QUE VOCÊ ENTREGA FUNCIONA ???

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

É SEMPRE BOM VER O RESULTADO DAQUILO QUE FAZEMOS. PORÉM …

Slide 6

Slide 6 text

ALGUMAS COISAS COMEÇAM A FICAR DIFÍCEIS DE SE ENTENDER …

Slide 7

Slide 7 text

ACTIVITY ( ~ 300 LoC) ADAPTER ( ~ 200 LoC) DATA ( ~ 150 LoC)

Slide 8

Slide 8 text

ACTIVITY ( ~ 300 LoC) ADAPTER ( ~ 300 LoC) DATA ( ~ 50 LoC) ANOTHER DATA ( ~ 50 LoC)

Slide 9

Slide 9 text

ACTIVITY ( ~ 300 LoC) MYADAPTER ( ~ 100 LoC) MYDATASOURCE ( ~ 50 LoC) BASE DATASOURCE ( ~ 50 LoC) BASEADAPTER ( ~ 250 LoC)

Slide 10

Slide 10 text

App (Visão Romântica)

Slide 11

Slide 11 text

CONTROLLER CONTROLLER CONTROLLER CONTROLLER CONTROLLER CONTROLLER CONTROLLER CONTROLLER CONTROLLER CONTROLLER CONTROLLER CONTROLLER App (Visão Real) SINGLETON

Slide 12

Slide 12 text

COISAS DIFÍCEIS DE SE EXPLICAR …

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

NÃO PRECISA SER ASSIM !!!

Slide 16

Slide 16 text

UMA IDÉIA SIMPLES

Slide 17

Slide 17 text

DELEGAR O TRABALHO DE VERIFICAÇÃO PARA UM ROBÔ

Slide 18

Slide 18 text

UMA QUEIXA COMUM NA COMUNIDADE MOBILE ?

Slide 19

Slide 19 text

TEM PELO MENOS UM UNIT TEST NO APP?

Slide 20

Slide 20 text

POR QUÊ ESCREVER TESTES

Slide 21

Slide 21 text

Documentação de alta fidelidade das decisões de API tomadas na base de código 1

Slide 22

Slide 22 text

Garantem e verificam comportamentos esperados 2

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Oferecem a condição correta para práticas de Continuous Integration e Continuous Delivery 4

Slide 25

Slide 25 text

Podem ser complementados para cobrir novos comportamentos, não esperados (bugs) ou não mapeados (funcionalidades) 5

Slide 26

Slide 26 text

Oferecem uma métrica fundamental para garantir a qualidade de produto 6

Slide 27

Slide 27 text

Mas nem tudo são flores …

Slide 28

Slide 28 text

É PRECISO PROJETAR SUAS APIs INTERNAS PARA QUE SEJAM BOAS

Slide 29

Slide 29 text

É PRECISO PENSAR NO DESIGN DAS SUAS CLASSES

Slide 30

Slide 30 text

Single Responsability Open-Closed Lyskov Substituition Interface Segregation Dependency Inversion

Slide 31

Slide 31 text

É PRECISO PENSAR EM QUE PAPÉIS SUAS CLASSES REPRESENTAM NA APLICAÇÃO

Slide 32

Slide 32 text

MVP MVVM VIPER FLUX REDUX DDD MVC … MVI

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

TESTES EXISTEM EM VÁRIOS SABORES

Slide 35

Slide 35 text

UNIT ACCEPTANCE E2E INTEGRATION Tempo de execução Custo de programação

Slide 36

Slide 36 text

NÃO HÁ SILVER BULLETS!

Slide 37

Slide 37 text

COMO TESTAR UM APP ANDROID ?

Slide 38

Slide 38 text

ESTUDO DE CASO

Slide 39

Slide 39 text

DEMO

Slide 40

Slide 40 text

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)

Slide 41

Slide 41 text

VIEW PROTOCOL VIEW IMPL. PRESENTER FlowableSubscriber Flowable LifecycleStrategist Disposable DisposeStrategy LifecycleObserver LifecycleOwner (eg. Activity) DATA SOURCE INFRASTRUCTURE Flowable

Slide 42

Slide 42 text

COMO TESTAR UMA UNIDADE SIMPLES ?

Slide 43

Slide 43 text

/** * Created by bira on 6/29/17. * * Handles any errors throwed by GSON and report an * UnexpectedResponse to the foward steps * */ public class DeserializationIssuesHandler implements FlowableTransformer {

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

private boolean isDeserializationError(Throwable throwable) { return throwable instanceof IllegalStateException || throwable instanceof JsonIOException || throwable instanceof JsonSyntaxException || throwable instanceof JsonParseException; }

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

DeserializationIssuesHandler handler = new DeserializationIssuesHandler<>(); private boolean checkHandledAsDeserializationError(Throwable throwable) { if (throwable instanceof UnexpectedResponseError) { return throwable.getMessage().contentEquals("Deserialization Error"); } return false; }

Slide 53

Slide 53 text

@Test public void shouldHandle_GsonThrowsIOException() { Flowable broken = Flowable.error(new JsonIOException("Cannot read file")); broken.compose(handler) .test() .assertError(this::checkHandledAsDeserializationError); }

Slide 54

Slide 54 text

@Test public void shouldNotHandle_OtherErrors() { IllegalAccessError ops = new IllegalAccessError(“OPS!"); Flowable broken = Flowable.error(ops); broken.compose(handler) .test() .assertError(throwable -> throwable.equals(ops)); }

Slide 55

Slide 55 text

COMO TESTAR A INFRAESTRUTURA DE DADOS EM NÍVEL DE INTEGRAÇÃO ?

Slide 56

Slide 56 text

public class TriviaInfrastructure implements GetRandomFacts { // Constructor and fields ... @Override public Flowable fetchTrivia() { List 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); }

Slide 57

Slide 57 text

public class TriviaInfrastructure implements GetRandomFacts { // Constructor and fields @Override public Flowable fetchTrivia() { List 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); }

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

@Test public void checkIntegration_404Error_AsNotFound() { server.enqueue(new MockResponse().setResponseCode(404)); infrastructure.fetchTrivia() .test() .assertNoValues() .assertError(ContentNotFoundError.class); }

Slide 64

Slide 64 text

@Test public void checkIntegration_404Error_AsNotFound() { server.enqueue(new MockResponse().setResponseCode(404)); infrastructure.fetchTrivia() .test() .assertNoValues() .assertError(ContentNotFoundError.class); }

Slide 65

Slide 65 text

@Test public void checkIntegration_5xxError_AsUnexpected() { server.enqueue(new MockResponse().setResponseCode(502)); infrastructure.fetchTrivia() .test() .assertNoValues() .assertError(UnexpectedResponseError.class); }

Slide 66

Slide 66 text

@Test public void checkIntegration_5xxError_AsUnexpected() { server.enqueue(new MockResponse().setResponseCode(502)); infrastructure.fetchTrivia() .test() .assertNoValues() .assertError(UnexpectedResponseError.class); }

Slide 67

Slide 67 text

COMO TESTAR UM COMPORTAMENTO DE APRESENTAÇÃO ISOLADAMENTE ?

Slide 68

Slide 68 text

public class LoadingCoordination implements FlowableTransformer { private LoadingView view; private Scheduler uiScheduler; // Constructor @Override public Publisher apply(Flowable upstream) { ShowAtStartHideWhenDone delegate = new ShowAtStartHideWhenDone<>( view.showLoading(), view.hideLoading(), uiScheduler ); return upstream.compose(delegate); } }

Slide 69

Slide 69 text

public class LoadingCoordinationTests { Scheduler uiScheduler = Schedulers.trampoline(); LoadingCoordination 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); }

Slide 70

Slide 70 text

public class LoadingCoordinationTests { Scheduler uiScheduler = Schedulers.trampoline(); LoadingCoordination 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); }

Slide 71

Slide 71 text

@Test public void shouldCoordinateLoading_WhenFlowEmmits() throws Exception { Flowable.just("A", "B", "C") .compose(loadingCoordination) .subscribe(); checkLoadingCoordinated(); } @Test public void shouldCoordinateLoading_WithEmptyFlow() throws Exception { Flowable empty = Flowable.empty(); empty.compose(loadingCoordination).subscribe(); checkLoadingCoordinated(); }

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

COMO TESTAR MÚLTIPLOS COMPORTAMENTOS DE APRESENTAÇÃO POR FLUXO ?

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

@Test public void shouldPresent_AvailableData_IntoView() throws Exception { Flowable 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(); }

Slide 82

Slide 82 text

@Test public void shouldPresent_AvailableData_IntoView() throws Exception { Flowable 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(); }

Slide 83

Slide 83 text

COMO TESTAR A LÓGICA ESPECÍFICA DE FRONT-END ?

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

COMO GARANTIR QUE O AS VIEWS SEGUEM O COMPORTAMENTO ESPERADO ?

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

CONCLUSÕES

Slide 97

Slide 97 text

SEM DESCULPAS PARA NÃO ESCREVER TESTES

Slide 98

Slide 98 text

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)

Slide 99

Slide 99 text

UBIRATAN SOARES Computer Scientist by ICMC/USP Software Engineer, curious guy Google Developer Expert for Android Teacher, speaker, etc, etc

Slide 100

Slide 100 text

OBRIGADO @ubiratanfsoares ubiratansoares.github.io https://br.linkedin.com/in/ubiratanfsoares