Testable Android apps

Testable Android apps

Diving into problems with building Android applications and refactoring it to Clean Architecture. We will discuss different approaches for building flexible and testable Android projects. I will show how to implement MVP for your application and how to test it in efficient way.

2b0404a5db1a74f01bf3bf94d142e28c?s=128

Alex Zhukovich

November 05, 2016
Tweet

Transcript

  1. 2.

    What does it mean testable? • Well structured source code

    • Test code base quickly • Test units in isolation way
  2. 3.

    Android World • Context matters • Huge Activity, Fragment, etc.

    • Fragmentation • Tests require device → Slow Android tests • Many apps are tested in manual way
  3. 4.

    "Standard" Android app • Business code in Activities, Fragments, Services,

    etc. • View and Presenter are mixed together • Hard to test
  4. 5.

    Clean Code • Try to write clean code • Separate

    your code • Avoid long methods • Avoid huge components (Activity, Fragment, etc)
  5. 6.

    Architectural requirements • Intelligibility • Testability • Independence from libraries

    and frameworks • Independence from data source • Independence from UI
  6. 8.

    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
  7. 9.

    MVP

  8. 11.

    View - interface public interface TemperatureConverterView { void displayResult(double value);

    void displayProgress(); void hideProgress(); void launchSettingsActivity(); void displayErrorMessage(String message); }
  9. 12.

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

    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)); }
  11. 14.

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

    Presenter implementation public class TemperatureConverterPresenterImpl implements MvpPresenter<TemperatureConverterView> { 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 */ } }
  13. 16.

    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); } }
  14. 19.
  15. 21.

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

    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/
  17. 23.

    Hamcrest + Espresso public static Matcher<Object> withToolbarTitle(final Matcher<String> textMatcher) {

    return new BoundedMatcher<Object, Toolbar>(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 ...
  18. 24.

    Testing a UI part @RunWith(AndroidJUnit4.class) public class TemperatureConverterActivityTest { @Rule

    public ActivityTestRule<TemperatureConverterActivity> 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)))); } }
  19. 25.

    Testing system stuff • Notifications • App Shortcut • Press

    hardware buttons • Interacting with any applications UIAutomator
  20. 26.

    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); } }
  21. 27.

    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(); } }
  22. 28.
  23. 29.

    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
  24. 34.

    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 } <module name="Checker"> <module name="FileLength"><property name="max" value="400"/></module> <module name="TreeWalker"> <module name="MethodLength"><property name="max" value="40"/></module> <module name="LineLength"><property name="max" value="120"/></module> <module name="ParameterNumber"><property name="max" value="4"/></module> <module name="RedundantImport"/> <module name="UnusedImports"/> </module> </module>
  25. 35.

    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