Slide 1

Slide 1 text

Testable Android apps ALIAKSANDR ZHUKOVICH @Alex_Zhukovich http://alexzh.com

Slide 2

Slide 2 text

What does it mean testable? • Well structured source code • Test code base quickly • Test units in isolation way

Slide 3

Slide 3 text

Android World • Context matters • Huge Activity, Fragment, etc. • Fragmentation • Tests require device → Slow Android tests • Many apps are tested in manual way

Slide 4

Slide 4 text

"Standard" Android app • Business code in Activities, Fragments, Services, etc. • View and Presenter are mixed together • Hard to test

Slide 5

Slide 5 text

Clean Code • Try to write clean code • Separate your code • Avoid long methods • Avoid huge components (Activity, Fragment, etc)

Slide 6

Slide 6 text

Architectural requirements • Intelligibility • Testability • Independence from libraries and frameworks • Independence from data source • Independence from UI

Slide 7

Slide 7 text

Clean Architecture https://github.com/android10/Android-CleanArchitecture

Slide 8

Slide 8 text

Layers Presentation layer View Presenter Domain layer Business objects Business logic Interactor Data layer Repository API Client interfaces DAO interfaces Cloud DB Memory INTERACTOR INTERFACE REPOSITORY INTERFACE

Slide 9

Slide 9 text

MVP

Slide 10

Slide 10 text

Model-View-Presenter View Presenter Model

Slide 11

Slide 11 text

View - interface public interface TemperatureConverterView { void displayResult(double value); void displayProgress(); void hideProgress(); void launchSettingsActivity(); void displayErrorMessage(String message); }

Slide 12

Slide 12 text

View implementation public class TemperatureConverterActivity extends AppCompatActivity implements TemperatureConverterView { @Inject TemperatureConverterPresenter mPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_temperature_converter); ... } @Override public void displayResult(double value) { mOutputView.setText(getString(R.string.output_text_format, value)); } @Override public void launchSettingsActivity() { startActivity(new Intent(this, SettingsActivity.class)); } @Override public void displayErrorMessage(String message) { Snackbar.make(mResultLayout, message, Snackbar.LENGTH_LONG).show(); } }

Slide 13

Slide 13 text

View → Presenter • View tells to Presenter what need to do use Lifecycle (onStart, onStop) and Android View events (OnClick, onClickItem). @OnClick(R.id.convertButton) public void onConvertButtonClicked() { mPresenter.convertTemperature( mInputView.getText().toString(), getTemperatureUnitFromSpinner(mInputUnitSpinner), getTemperatureUnitFromSpinner(mOutputUnitSpinner)); }

Slide 14

Slide 14 text

Presenter • There can be memory leaks in Presenter • attachView(View view); • detachView(); @Override protected void onStart() { super.onStart(); mPresenter.attachView(this); } @Override protected void onStop() { mPresenter.detachView(); super.onStop(); }

Slide 15

Slide 15 text

Presenter implementation public class TemperatureConverterPresenterImpl implements MvpPresenter { private TemperatureConverterView mView; private EventBus mEventBus; private ConvertTemperatureUseCase mConvertTemperatureUseCase; public TemperatureConverterPresenterImpl(ConvertTemperature convertTemperature, EventBus eventBus) { this.mConvertTemperatureUseCase = convertTemperature; this.mEventBus = eventBus; } @Override public void attachView(TemperatureConverterView mvpView) { /* attach view and register event bus */ } public void convertTemperature() { ... mConvertTemperatureUseCase.execute(new InputData(inputValue, from, to)); } @Override public void detachView() { /* detach view and unregister eventbus */ } }

Slide 16

Slide 16 text

UseCase public class ConvertTemperatureUseCaseImpl extends UseCase implements ConvertTemperatureUseCase { @Override public void run() { mConverterTemperatureFactory.getTemperatureConverter().convertData(mInputData, new Callback() { @Override public void onResult(final TemperatureConvertedSuccessful convertedSuccessful) { getMainThreadExecutor().execute(new Runnable() { @Override public void run() { mEventBus.post(convertedSuccessful); } }); } }); } @Override public void execute(InputData data) { this.mInputData = data; getInteractorExecutor().run(this); } }

Slide 17

Slide 17 text

View and business logic NO

Slide 18

Slide 18 text

Flow Presenter Use case Repository Web service Local

Slide 19

Slide 19 text

Testing

Slide 20

Slide 20 text

Hamcrest library

Slide 21

Slide 21 text

Testing a presenter @RunWith(MockitoJUnitRunner.class) public class TemperatureConverterPresenterTest { @Mock TemperatureConverterView mView; @Mock EventBus mEventBus; private TemperatureConverterPresenter mPresenter; @Test public void shouldVerifyConvertDataSuccessfulWithCorrectView() { when(mTemperatureFactory.getTemperatureConverter()).thenReturn(mConverter); doNothing().when(mConverter) .convertData(any(InputData.class), any(ConvertTemperatureUseCase.Callback.class)); mPresenter.attachView(mView); mPresenter.convertTemperature(INPUT_VALUE_STR, FROM_TEMPERATURE_UNIT, TO_TEMPERATURE_UNIT); mPresenter.detachView(); verify(mEventBus).register(any()); verify(mView).displayProgress(); verify(mEventBus).unregister(any()); } }

Slide 22

Slide 22 text

Recommendation for setting up the device / emulator One small recommendation for avoiding flakiness is turning off animation on real or virtual devices. • Window animation scale • Transition animation scale • Animator duration scale • Settings / Developer Options/

Slide 23

Slide 23 text

Hamcrest + Espresso public static Matcher withToolbarTitle(final Matcher textMatcher) { return new BoundedMatcher(Toolbar.class) { @Override public boolean matchesSafely(Toolbar toolbar) { return textMatcher.matches(toolbar.getTitle()); } @Override public void describeTo(Description description) { description.appendText("with toolbar title: ") textMatcher.describeTo(description); } }; } onView(withId(R.id.toolbar)) .check(matches(withToolbarTitle( is (TITLE)))); startsWith endsWith ...

Slide 24

Slide 24 text

Testing a UI part @RunWith(AndroidJUnit4.class) public class TemperatureConverterActivityTest { @Rule public ActivityTestRule mRule = new ActivityTestRule<>(TemperatureConverterActivity.class); @Test public void shouldVerifyConvertingFromCelsiusToFahrenheit() { onView(withId(R.id.inputView)) .perform(typeText(String.valueOf(CELSIUS_VALUE)), closeSoftKeyboard()); setTemperatureUnit(R.id.inputTemperatureSpinner, CELSIUS_STR); setTemperatureUnit(R.id.outputTemperatureSpinner, FAHRENHEIT_STR); onView(withId(R.id.convertButton)) .perform(click()); onView(withId(R.id.outputView)) .check(matches(isDisplayed())); onView(withId(R.id.outputView)) .check(matches(withText(getOutputString(FAHRENHEIT_VALUE)))); } }

Slide 25

Slide 25 text

Testing system stuff • Notifications • App Shortcut • Press hardware buttons • Interacting with any applications UIAutomator

Slide 26

Slide 26 text

App Shortcut @RunWith(AndroidJUnit4.class) @SdkSuppress(minSdkVersion = 25) public class AppShortcutTest { private UiDevice mDevice; @Before public void setup() { mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); mDevice.pressHome(); mDevice.findObject(By.desc(APPS)).click(); mDevice.wait(Until.hasObject(By.res(PACKAGE_NEXUS_LAUNCHER, SEARCH_BOX_NEXUS_LAUNCHER)), TIMEOUT); UiObject2 search = mDevice.findObject(By.res(PACKAGE_NEXUS_LAUNCHER, SEARCH_BOX_NEXUS_LAUNCHER)); search.click(); search.setText(APP_NAME); } @Test public void shouldVerifyCelsiusToFahrenheitShortcut() { mDevice.findObject(By.desc(APP_NAME)).longClick(); mDevice.wait(Until.hasObject(By.desc(CONVERT_CELSIUS_TO_FAHRENHEIT_DESC)), TIMEOUT); mDevice.findObject(By.desc(CONVERT_CELSIUS_TO_FAHRENHEIT_DESC)).click(); verifyInputAndOutputUnits(CELSIUS_UNIT, FAHRENHEIT_UNIT); } }

Slide 27

Slide 27 text

Notifications RunWith(AndroidJUnit4.class) @SdkSuppress(minSdkVersion = 18) public class CoffeeOrderDetailsActivityTest { @Test public void shouldOrderCoffeeAndVerifyNotification() { verifyCoffeeOrder(); onView(withId(R.id.pay)).perform(click()); mDevice.openNotification(); mDevice.wait(Until.hasObject(By.text(NOTIFICATION_TITLE)), TIMEOUT); UiObject2 title = device.findObject(By.text(NOTIFICATION_TITLE)); UiObject2 text = device.findObject(By.text(NOTIFICATION_TEXT)); assertEquals(NOTIFICATION_TITLE, title.getText()); assertEquals(NOTIFICATION_TEXT, text.getText()); title.click(); mDevice.wait(Until.hasObject(By.text(ESPRESSO.getName())), TIMEOUT); onView(withId(R.id.delivery_info)) .check(matches(withText(mActivityRule.getActivity() .getString(R.string.deliver_to_username, DELIVERY_INFO)))); verifyCoffeeOrder(); } }

Slide 28

Slide 28 text

Packaging

Slide 29

Slide 29 text

Package by layer data (module) repository database api cache exception domain (module) model interactor executor abstraction presenter (module) presenter view navigation app data repository database api cache exception domain model interactor executor abstraction presenter presenter view navigation

Slide 30

Slide 30 text

Package by feature app core data utils posts detail profile notification settings

Slide 31

Slide 31 text

Continuous Integration

Slide 32

Slide 32 text

Continuous Integration • Jenkins • TeamCity • CircleCI • etc

Slide 33

Slide 33 text

CircleCI circle.yml

Slide 34

Slide 34 text

Codestyle checking apply plugin: 'checkstyle' task checkstyle(type: Checkstyle) { description 'Checks that the code meets standards' group 'verification' configFile file('./qa-check/checkstyle.xml') source 'src' include '**/*.java' exclude '**/gen/**' classpath = files() ignoreFailures = false }

Slide 35

Slide 35 text

Q & A • The Clean Architecture (Robert Martin/Uncle Bob) https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html • S is for Single Responsibility Principle (Donn Felker) https://realm.io/news/donn-felker-solid-part-1/ • Android Testing Codelab https://codelabs.developers.google.com/codelabs/android-testing/ • Automating User Interface Tests https://developer.android.com/training/testing/ui-testing/index.html • Project https://github.com/AlexZhukovich/TemperatureConverterTDD @Alex_Zhukovich