Slide 1

Slide 1 text

Testing on Android

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

About us Consulting Staffing In-house projects

Slide 4

Slide 4 text

AppFoundry appfoundry.be

Slide 5

Slide 5 text

Testing

Slide 6

Slide 6 text

Testing

Slide 7

Slide 7 text

Testing

Slide 8

Slide 8 text

Confidence Refactor possible Code handover Fix bugs once

Slide 9

Slide 9 text

Android history

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

Instrumentation Process Test package InstrumentationTestRunner Application package Test case classes JUnit

Slide 13

Slide 13 text

// 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

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

Testing tools Android Studio / Gradle Android Testing Support Library Espresso

Slide 17

Slide 17 text

AndroidJUnitRunner Espresso UIAutomator

Slide 18

Slide 18 text

Unit tests Integration / UI tests Other Unit tests

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Mockable Android Jar

Slide 22

Slide 22 text

Demo notes app

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Model View Presenter Model Presenter View

Slide 25

Slide 25 text

Model View Presenter Model Presenter View Android UI

Slide 26

Slide 26 text

Dependency injection Dagger

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

Expressiveness 

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Hamcrest Lots of useful matchers Ŷ Custom matchers Ŷ not a number public void testSquareRootOfMinusOneIsNotANumber() {
 assertThat(Math.sqrt(-1), is(notANumber()));
 }

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Espresso: add note

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Espresso: Intents

Slide 50

Slide 50 text

Espresso: Intents

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Testing network resources

Slide 57

Slide 57 text

Mocking the Retrofit interface MockRestAdapter sample

Slide 58

Slide 58 text

Mocking the Retrofit interface Retrofit mock public interface GitHub {
 @GET("/repos/{owner}/{repo}/contributors")
 List contributors(@Path("owner") String owner, @Path("repo") String repo);
 }

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

Unit tests Integration / UI tests Other Performance tests

Slide 66

Slide 66 text

Performance Testing Harness

Slide 67

Slide 67 text

Performance

Slide 68

Slide 68 text

Performance

Slide 69

Slide 69 text

Custom Test Rules @Rule
 public EnableTestTracing enableTestTracing;
 
 @Rule
 public EnablePostTestDumpsys enablePostTestDumpsys;
 
 @Rule
 public EnableLogcatDump enableLogcatDump;
 
 @Rule
 public EnableNetStatsDump enableNetStatsDump;

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

Tips & tricks

Slide 72

Slide 72 text

Unit tests Integration / UI tests Other Other tests

Slide 73

Slide 73 text

The Monkey

Slide 74

Slide 74 text

evil monkey…

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Test Driven Development RED FAILING TEST GREEN FIXED TEST BLUE REFACTOR

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

Physical devices

Slide 80

Slide 80 text

30 phones - tablets

Slide 81

Slide 81 text

Test devices

Slide 82

Slide 82 text

Remote

Slide 83

Slide 83 text

No content

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Reduce risk Continuous integration Reduce overhead Quality Assurance

Slide 87

Slide 87 text

Automate all the things

Slide 88

Slide 88 text

Jenkins

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

Metrics

Slide 91

Slide 91 text

Dashboard

Slide 92

Slide 92 text

Dashing

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

Continuous Delivery

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

No content

Slide 99

Slide 99 text

No content

Slide 100

Slide 100 text

Test app distribution

Slide 101

Slide 101 text

Test app distribution

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

Resources

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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)

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

No content

Slide 111

Slide 111 text

Thank you!