$30 off During Our Annual Pro Sale. View Details »

Testing on Android at KdG

AppFoundry
February 02, 2016

Testing on Android at KdG

A presentation by Filip Maelbrancke (AppFoundry) for the Karel de Grote University College (KdG)

AppFoundry

February 02, 2016
Tweet

More Decks by AppFoundry

Other Decks in Programming

Transcript

  1. Testing on Android

    View Slide

  2. Your host
    Filip Maelbrancke
    Consultant @ AppFoundry
    [email protected]
    @fmaelbrancke

    View Slide

  3. About us
    Consulting Staffing In-house projects

    View Slide

  4. AppFoundry
    appfoundry.be

    View Slide

  5. Testing

    View Slide

  6. Testing

    View Slide

  7. Testing

    View Slide

  8. Confidence
    Refactor possible
    Code handover
    Fix bugs once

    View Slide

  9. Android history

    View Slide

  10. View Slide

  11. View Slide

  12. Instrumentation
    Process
    Test package InstrumentationTestRunner Application package
    Test case classes JUnit

    View Slide

  13. // Start the main activity of the application under test

    mActivity = getActivity();

    // Get a handle to the Activity object's main UI widget, a Spinner

    mSpinner = (Spinner) mActivity.findViewById(com.android.example.spinner.R.id.Spinner);

    // Set the Spinner to a known position

    mActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION);

    // Stop the activity - The onDestroy() method should save the state of the Spinner

    mActivity.finish();

    // Re-start the Activity - the onResume() method should restore the state of the Spinner

    mActivity = getActivity();

    // Get the Spinner's current position

    int currentPosition = mActivity.getSpinnerPosition();

    // Assert that the current position is the same as the starting position

    assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition);
    Test Activity state saving and restoration

    View Slide

  14. View Slide

  15. View Slide

  16. Testing tools
    Android Studio /
    Gradle
    Android Testing
    Support Library
    Espresso

    View Slide

  17. AndroidJUnitRunner
    Espresso
    UIAutomator

    View Slide

  18. Unit tests
    Integration / UI
    tests
    Other
    Unit tests

    View Slide

  19. (UNIT)
    TESTS
    ISOLATED REPEATABLE FAST SELF-DOCUMENTING
    ž
    Test traits

    View Slide

  20. Easy to execute
    Easy to debug
    Realistic
    Fluent API
    Device, emulator, …

    View Slide

  21. Mockable Android Jar

    View Slide

  22. Demo notes app

    View Slide

  23. Google Codelabs
    https://www.code-labs.io/

    View Slide

  24. Model View Presenter
    Model
    Presenter
    View

    View Slide

  25. Model View Presenter
    Model
    Presenter
    View
    Android
    UI

    View Slide

  26. Dependency injection
    Dagger

    View Slide

  27. View Slide

  28. Expressiveness

    View Slide

  29. Hamcrest
    assertThat(audience, is(payingAttention()))
    Verification
    Mocking
    Assertions

    View Slide

  30. Hamcrest
    Library of matchers
    Ŷ Syntactic sugar
    Ŷ
    Assert
    assertThat(someString, is(equalTo(“Expected”)));
    actual value expectation on the value (Matcher)

    View Slide

  31. Hamcrest
    Assert
    Hamcrest
    assertEquals(“Conversion failed”, “Expected”, someString);
    assertThat(someString, is(equalTo(“Expected”)));

    View Slide

  32. Hamcrest: collections
    Assert
    assertEquals(person, bigbangCharacters.getPersons().iterator().next());
    Hamcrest
    assertThat(bigbangCharacters.getPersons(), hasItem(person));

    View Slide

  33. Hamcrest matchers
    Core
    anything

    is


    Logical
    allOf

    anyOf

    not
    Object
    equalTo

    instanceOf

    (not)nullValue
    Custom Beans
    hasProperty
    Text
    equalToIgnoringCase

    startsWith


    Number
    greaterThan(OrEqualTo)

    closeTo
    Collections
    hasItem, hasItems

    hasItemInArray

    View Slide

  34. Hamcrest
    Lots of useful matchers
    Ŷ Custom matchers
    Ŷ
    not a number
    public void testSquareRootOfMinusOneIsNotANumber() {

    assertThat(Math.sqrt(-1), is(notANumber()));

    }

    View Slide

  35. Hamcrest custom matcher
    Hamcrest matcher
    /**

    * Hamcrest matcher: test if a double has the value NaN (not a number)

    */

    public class IsNotANumber extends TypeSafeMatcher {


    @Override

    public boolean matchesSafely(Double number) {

    return number.isNaN();

    }


    public void describeTo(Description description) {

    description.appendText("not a number");

    }


    @Factory

    public static Matcher notANumber() {

    return new IsNotANumber();

    }

    }

    View Slide

  36. View Slide

  37. Find, perform, check
    onView(ViewMatcher)
    .perform(ViewAction)
    .check(ViewAssertion);

    View Slide

  38. Espresso
    Espresso
    onView(withId(R.id.some_id)).perform(click());


    onView(withText("Hi there”)).check(matches(isDisplayed()));


    onView(withId(R.id.some_id)).check(matches(withText(containsString(“someString”))));


    onData(is(someObject)).perform(click());

    View Slide

  39. Espresso API
    onView(Matcher)
    onData(Matcher)
    onView(Matcher)
    onView(Matcher)
    withId() / withText()
    is() / instanceOf()
    click() / enterText() / scrollTo()
    matches(Matcher)
    isDisplayed / isEnabled / hasFocus
    withText / …
    Find stuff Do stuff
    Check

    View Slide

  40. Espresso cheat sheet
    https://github.com/googlesamples/android-testing/tree/master/downloads

    View Slide

  41. Espresso: add note

    View Slide

  42. Espresso test anatomy
    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class NotesScreenTest {
    @Rule
    public ActivityTestRule notesActivityTestRule =
    new ActivityTestRule<>(NotesActivity.class);

    View Slide

  43. Espresso test anatomy
    @RunWith(AndroidJUnit4.class)
    @LargeTest
    public class NotesScreenTest {
    @Rule
    public ActivityTestRule notesActivityTestRule =
    new ActivityTestRule<>(NotesActivity.class);

    View Slide

  44. Espresso: exercise UI
    @Test

    public void addNoteToNotesList() throws Exception {


    // Click on the add note button

    onView(withId(R.id.fab_add_notes)).perform(click());


    // Add note title and description

    onView(withId(R.id.add_note_title)).perform(typeText(“More”));

    onView(withId(R.id.add_note_description)).perform(typeText(“Testing”));


    // Save the note

    onView(withId(R.id.fab_add_notes)).perform(click());


    // Verify note is displayed on screen

    onView(withItemText(“Testing”)).check(matches(isDisplayed()));

    }

    View Slide

  45. Espresso: exercise UI
    @Test

    public void addNoteToNotesList() throws Exception {


    // Click on the add note button

    onView(withId(R.id.fab_add_notes)).perform(click());


    // Add note title and description

    onView(withId(R.id.add_note_title)).perform(typeText(“More”));

    onView(withId(R.id.add_note_description)).perform(typeText(“Testing”));


    // Save the note

    onView(withId(R.id.fab_add_notes)).perform(click());


    // Verify note is displayed on screen

    onView(withItemText(“Testing”)).check(matches(isDisplayed()));

    }

    View Slide

  46. Espresso: exercise UI
    @Test

    public void addNoteToNotesList() throws Exception {


    // Click on the add note button

    onView(withId(R.id.fab_add_notes)).perform(click());


    // Add note title and description

    onView(withId(R.id.add_note_title)).perform(typeText(“More”));

    onView(withId(R.id.add_note_description)).perform(typeText(“Testing”));


    // Save the note

    onView(withId(R.id.fab_add_notes)).perform(click());


    // Verify note is displayed on screen

    onView(withItemText(“Testing”)).check(matches(isDisplayed()));

    }

    View Slide

  47. View Slide

  48. Screen Object Pattern
    public class LoginScreen {


    private static final ViewInteraction USERNAME_EDIT = onView(withId(R.id.login_username));

    private static final ViewInteraction PASSWORD_EDIT = onView(withId(R.id.login_password));

    private static final ViewInteraction LOGIN_BUTTON = onView(withId(R.id.login_button));

    public static final ViewInteraction LOGIN_VALIDATION_USERNAME_INVALID_FORMAT = onView(withId(R.id.login_validation_username));

    public static final ViewInteraction LOGIN_VALIDATION_INVALID_PASSWORD = onView(withId(R.id.login_validation_password));


    public void typeUsername(String username) {

    USERNAME_EDIT.perform(clearText(), typeText(username));

    }


    public void typePassword(String password) {

    PASSWORD_EDIT.perform(clearText(), typeText(password));

    }


    public void clickLoginButton() {

    LOGIN_BUTTON.perform(scrollTo(), click());

    }


    public void doLogin(String username, String password) {

    typeUsername(username);

    typePassword(password);

    clickLoginButton();

    }

    }

    View Slide

  49. Espresso: Intents

    View Slide

  50. Espresso: Intents

    View Slide

  51. Intent validation
    @Test
    public void validateIntentSentToPackage() {
    // User action that results in an external "phone" activity being launched.
    user.clickOnView(system.getView(R.id.callButton));
    // Using a canned RecordedIntentMatcher to validate that an intent resolving
    // to the "phone" activity has been sent.
    intended(toPackage("com.android.phone"));
    }

    View Slide

  52. Intent stubbing (Mockito for Intents)
    @Test
    public void activityResult_IsHandledProperly() {
    // Build a result to return when a particular activity is launched.
    Intent resultData = new Intent();
    String phoneNumber = "012-345-678";
    resultData.putExtra("phone", phoneNumber);
    ActivityResult result = new ActivityResult(Activity.RESULT_OK, resultData);
    // Set up result stubbing when an intent sent to "contacts" is seen.
    intending(toPackage("com.android.contacts")).respondWith(result));
    // User action that results in "contacts" activity being launched.
    // Launching activity expects phoneNumber to be returned and displays it on the screen.
    onView(withId(R.id.pickButton)).perform(click());
    // Assert that data we set up above is shown.
    onView(withId(R.id.phoneNumber).check(matches(withText(phoneNumber)));
    }

    View Slide

  53. Intent matchers
    intended(allOf(
    hasAction(Intent.ACTION_CHOOSER),
    hasExtra(is(Intent.EXTRA_INTENT),
    allOf(
    hasAction(Intent.ACTION_SEND),
    hasExtra(Intent.EXTRA_TEXT, "Expected url") ))));

    View Slide

  54. Espresso cheat sheet
    https://github.com/googlesamples/android-testing/tree/master/downloads

    View Slide

  55. Espresso synchronization
    public class IdleMonitor implements IdlingResource {


    @Override

    public String getName() {

    return IdleMonitor.class.getSimpleName();

    }


    @Override

    public boolean isIdleNow() {

    // return true if resource is idle

    return false;

    }


    @Override

    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {

    // store a reference to the resourceCallback

    // notify resourceCallback when idle

    }

    }
    Espresso.registerIdlingResources(idlingResource);

    View Slide

  56. Testing network resources

    View Slide

  57. Mocking the Retrofit interface
    MockRestAdapter sample

    View Slide

  58. Mocking the Retrofit interface
    Retrofit mock
    public interface GitHub {

    @GET("/repos/{owner}/{repo}/contributors")

    List contributors(@Path("owner") String owner, @Path("repo") String repo);

    }

    View Slide

  59. Mocking the Retrofit interface
    Canned data
    /** A mock implementation of the {@link GitHub} API interface. */

    static class MockGitHub implements GitHub {

    private final Map>> ownerRepoContributors;


    public MockGitHub() {

    ownerRepoContributors = new LinkedHashMap>>();


    // Seed some mock data.

    addContributor("square", "retrofit", "John Doe", 12);

    addContributor("square", "retrofit", "Bob Smith", 2);

    addContributor("square", "retrofit", "Big Bird", 40);

    addContributor("square", "picasso", "Proposition Joe", 39);

    addContributor("square", "picasso", "Keiser Soze", 152);

    }

    View Slide

  60. Mocking the Retrofit interface
    Mock
    @Override
    public List contributors(String owner, String repo) {

    Map> repoContributors = ownerRepoContributors.get(owner);

    if (repoContributors == null) {

    return Collections.emptyList();

    }

    List contributors = repoContributors.get(repo);

    if (contributors == null) {

    return Collections.emptyList();

    }

    return contributors;

    }

    View Slide

  61. Mocking the Retrofit interface
    Mock
    // Create a very simple REST adapter which points to the GitHub API endpoint.

    RestAdapter restAdapter = new RestAdapter.Builder()

    .setEndpoint(API_URL)

    .build();


    // Wrap our REST adapter to allow mock implementations and fake network delay.

    MockRestAdapter mockRestAdapter = MockRestAdapter.from(restAdapter);


    // Instantiate a mock object so we can interact with it later.

    MockGitHub mockGitHub = new MockGitHub();

    // Use the mock REST adapter and our mock object to create the API interface.

    GitHub gitHub = mockRestAdapter.create(GitHub.class, mockGitHub);


    // Query for some contributors for a few repositories.

    printContributors(gitHub, "square", "retrofit");

    printContributors(gitHub, "square", "picasso");

    View Slide

  62. Wiremock
    {
    "request": {
    "method": "GET",
    "url": "/api/mytest"
    },
    "response": {
    "status": 200,
    "body": "More content\n"
    }
    }

    View Slide

  63. Wiremock
    stubFor(get(urlEqualTo("/some/thing"))
    .willReturn(aResponse()
    .withStatus(404)
    .withBody(“No!!”)));

    View Slide

  64. OKHTTP MockWebServer
    public void testServerErrorShouldShowEmptyState() {
    MockServiceModule mockServiceModule = new MockServiceModule();
    DaggerHelper.initWithTestModules(mockServiceModule);
    // Setup the MockWebServer
    MockResponse mockResponse = new MockResponse();
    mockResponse.setResponseCode(500);
    mockServiceModule.getMockWebServer().enqueue(mockResponse);
    // mockwebserver + restadapter are setup, now init activity
    getActivity();
    onView(withId(R.id.list_empty_image)).check(
    matches(isDisplayed()));
    }

    View Slide

  65. Unit tests
    Integration / UI
    tests
    Other
    Performance tests

    View Slide

  66. Performance Testing Harness

    View Slide

  67. Performance

    View Slide

  68. Performance

    View Slide

  69. Custom Test Rules
    @Rule

    public EnableTestTracing enableTestTracing;


    @Rule

    public EnablePostTestDumpsys enablePostTestDumpsys;


    @Rule

    public EnableLogcatDump enableLogcatDump;


    @Rule

    public EnableNetStatsDump enableNetStatsDump;

    View Slide

  70. View Slide

  71. Tips & tricks

    View Slide

  72. Unit tests
    Integration / UI
    tests
    Other
    Other tests

    View Slide

  73. The Monkey

    View Slide

  74. evil monkey…

    View Slide

  75. Awaitility
    @Test

    public void test_play_liveStream() {


    MediaControllerCompat mediaController = new MediaControllerCompat(getContext(), audioPlayerService.getMediaSessionToken());

    String uri = "http://stream.be/service/mp3:web/program_ondemand_128.mp3/playlist.m3u8";

    Bundle extras = new Bundle();

    extras.putBoolean(AudioPlayerService.EXTRA_CONTINUE_ON_RESUME, false);

    mediaController.getTransportControls().playFromMediaId(uri, extras);
    await().atMost(10, TimeUnit.SECONDS).until(musicIsActive());


    }
    Test asynchronous system
    Express expectations in easy to read manner

    View Slide

  76. Awaitility
    @Test

    public void test_play_liveStream() {


    MediaControllerCompat mediaController = new MediaControllerCompat(getContext(), audioPlayerService.getMediaSessionToken());

    String uri = "http://stream.be/service/mp3:web/program_ondemand_128.mp3/playlist.m3u8";

    Bundle extras = new Bundle();

    extras.putBoolean(AudioPlayerService.EXTRA_CONTINUE_ON_RESUME, false);

    mediaController.getTransportControls().playFromMediaId(uri, extras);

    await().atMost(10, TimeUnit.SECONDS).until(musicIsActive());


    }
    Test asynchronous system
    Express expectations in easy to read manner

    View Slide

  77. Test Driven Development
    RED
    FAILING TEST
    GREEN
    FIXED TEST
    BLUE
    REFACTOR

    View Slide

  78. Automatic
    On every commit / scheduled
    Unit tests
    UI integration tests
    with Espresso

    View Slide

  79. Physical devices

    View Slide

  80. 30 phones - tablets

    View Slide

  81. Test devices

    View Slide

  82. Remote

    View Slide

  83. View Slide

  84. View Slide

  85. Creating software = complex
    Continuous
    integration Ensure quality
    Automate
    high-quality, robust and reliable apps
    tedious / error-prone activities

    View Slide

  86. Reduce risk
    Continuous
    integration Reduce overhead
    Quality Assurance

    View Slide

  87. Automate all the things

    View Slide

  88. Jenkins

    View Slide

  89. Continuous integration
    1
    2
    3
    4
    CODE & COMMIT BUILD & CHECK
    CI PICKUP
    REPORT RESULTS

    View Slide

  90. Metrics

    View Slide

  91. Dashboard

    View Slide

  92. Dashing

    View Slide

  93. Build pipeline
    Checkout /
    compile
    Unit tests
    Test
    coverage
    Code
    analysis
    Create
    deployable
    artifact
    Deploy for
    automatic
    QA test
    Trigger
    automated
    QA stage

    View Slide

  94. Auto publish
    Delivery
    Promote app to production
    Automate
    Google Play alpha / beta
    iTunes Connect
    without additional tools

    View Slide

  95. Continuous
    Delivery

    View Slide

  96. Continuous delivery / deliverable
    Continuous
    Delivery
    Deliverable Push on demand
    Confidence
    of being deployable

    View Slide

  97. Feature-based deployment
    A successful Git branching model
    http://nvie.com/posts/a-successful-git-branching-model/

    View Slide

  98. View Slide

  99. View Slide

  100. Test app distribution

    View Slide

  101. Test app distribution

    View Slide

  102. Internal QA
    Testing
    Internal alpha testing program
    Beta testing / staged rollout
    of being deployable

    View Slide

  103. Resources

    View Slide

  104. Android Testing Support Library
    https://google.github.io/android-testing-support-library/

    View Slide

  105. Google Codelabs
    https://www.code-labs.io/

    View Slide

  106. Google Testing Samples / Blueprint
    https://github.com/googlesamples/android-testing/ https://github.com/googlesamples/android-testing-templates/

    View Slide

  107. Resources
    Wiremock
    http://wiremock.org/
    OKHTTP MockWebServer
    https://github.com/square/okhttp/tree/master/mockwebserver
    UI/Application Exerciser Monkey
    https://developer.android.com/tools/help/monkey.html
    Awaitility
    https://github.com/jayway/awaitility

    View Slide

  108. Suggested reading
    Test Driven Development: By Example
    Beck, Kent (978-0321146533)
    Continuous Integration: Improving Software Quality and
    Reducing Risk
    Duvall, Paul M. et al. (978-0321336385)
    Working Effectively with Legacy Code
    Feathers, Michael (978-0131177055)

    View Slide

  109. Questions?
    Filip Maelbrancke
    Consultant @ AppFoundry
    [email protected]
    @fmaelbrancke

    View Slide

  110. View Slide

  111. Thank you!

    View Slide