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

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.

Alex Zhukovich

November 05, 2016
Tweet

More Decks by Alex Zhukovich

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  9. MVP

    View Slide

  10. Model-View-Presenter
    View Presenter Model

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. 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 */
    }
    }

    View Slide

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

    View Slide

  17. View and business logic
    NO

    View Slide

  18. Flow
    Presenter
    Use case
    Repository
    Web
    service
    Local

    View Slide

  19. Testing

    View Slide

  20. Hamcrest library

    View Slide

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

    View Slide

  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/

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  28. Packaging

    View Slide

  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

    View Slide

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

    View Slide

  31. Continuous Integration

    View Slide

  32. Continuous Integration
    • Jenkins
    • TeamCity
    • CircleCI
    • etc

    View Slide

  33. CircleCI
    circle.yml

    View Slide

  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
    }










    View Slide

  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

    View Slide