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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

  9. Model-View-Presenter
    View Presenter Model

    View full-size slide

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

    View full-size slide

  11. 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 full-size slide

  12. 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 full-size slide

  13. 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 full-size slide

  14. 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 full-size slide

  15. 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 full-size slide

  16. View and business logic
    NO

    View full-size slide

  17. Flow
    Presenter
    Use case
    Repository
    Web
    service
    Local

    View full-size slide

  18. Hamcrest library

    View full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

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

    View full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

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

    View full-size slide

  28. Continuous Integration

    View full-size slide

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

    View full-size slide

  30. CircleCI
    circle.yml

    View full-size slide

  31. 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 full-size slide

  32. 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 full-size slide