Slide 1

Slide 1 text

10-12 September 2015 droidcon Greece Thessaloniki

Slide 2

Slide 2 text

Clean and Useful Android Testing, with Spock, Espresso and UI Automator! Jon Reeve, Wasabi Code, @themightyjon

Slide 3

Slide 3 text

(Gartner) Hype Cycle Gartner Research's Hype Cycle diagram / Jeremy Kemp / CC-BY-SA-3.0

Slide 4

Slide 4 text

Testing Enthusiasm Curve Plains of Indifference Peak of Religious Zeal Trough of Disillusionment Plateau of Productivity ( / Pragmatism)

Slide 5

Slide 5 text

● Android testing samples & blueprint https:/ /github.com/googlesamples/android-testing-templates https:/ /github.com/googlesamples/android-testing ● Others https:/ /github.com/AndroidBootstrap/android-bootstrap https:/ /github.com/JakeWharton/u2020 ● Many more... Where to Start? Examples...

Slide 6

Slide 6 text

Mike Cohn's Testing Pyramid What Do We Test?

Slide 7

Slide 7 text

The often encountered “Testing Ice-Cream Cone” What Do We Test?

Slide 8

Slide 8 text

● Why? ● Challenges ● Helpful Architecture ● Bad Test → Good Tests ● Spock Unit Tests

Slide 9

Slide 9 text

● Value more obvious to developers → Can suffer under pressure to do things quickly. ● Drive clean code: loose coupling, separation of concerns, etc. ● Immediate & clear failure: where and why. Why We Write Unit Tests

Slide 10

Slide 10 text

● Can be hard. Has got better: ● Real unit tests vs “Unit Android Tests”, off-device, faster. ● Mockable Android JAR. Death to RuntimeException: “Stub!” ● But still: ● Abstract classes over interfaces ● Big classes, combining many concerns ● Hidden internals: constants, functions, whole classes. ● “final” things Android Unit Testing Challenges

Slide 11

Slide 11 text

● MVP ● Wrap & Abstract ● Dependency Inversion ● Hexagonal Architecture / Ports & Adapters / “Clean Architecture” ● Rule out & wall off “hard to test” ● ... Basically, separation of concerns, divide by layers. How To Overcome?

Slide 12

Slide 12 text

● Activity or Fragment is simple, dumb “View”. ● Business logic in Presenter, isolated from platform. Unit testable. ● Good examples elsewhere: ● Shazam: http:/ /www.infoq.com/presentations/mobile-shazam ● Teh Interwebs: http:/ /lmgtfy.com/?q=mvp+android MVP

Slide 13

Slide 13 text

● Wrap static function call with Interface + implementation, to “loosen” coupling. ● System.currentTimeMillis() → Clock { long currentTimeMillis() } ● Wrap a system class to hide use of Android classes or functions ● Bit much? ● Mobile device, varying spec (low end devices). ● GC, tight loops. ● Be aware of context, and use where appropriate. (I ❤ enum) Wrap + Abstract

Slide 14

Slide 14 text

● Why? ● Challenges ● Helpful Architecture ● Bad Test → Good Tests ● Spock Unit Tests

Slide 15

Slide 15 text

public void sharedPreferencesHelper_SaveAndReadPersonalInformation() { // Save the personal information to SharedPreferences boolean success = mMockSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); assertThat("Checking that SharedPreferenceEntry.save... returns true", success, is(true)); // Read personal information from SharedPreferences SharedPreferenceEntry savedSharedPreferenceEntry = mMockSharedPreferencesHelper.getPersonalInfo(); // Make sure both written and retrieved personal information are equal. assertThat("Checking that SharedPreferenceEntry.name has been persisted and read correctly", mSharedPreferenceEntry.getName(), is(equalTo(savedSharedPreferenceEntry.getName()))); assertThat("Checking that SharedPreferenceEntry.dateOfBirth has been persisted and read " + "correctly", mSharedPreferenceEntry.getDateOfBirth(), is(equalTo(savedSharedPreferenceEntry.getDateOfBirth()))); assertThat("Checking that SharedPreferenceEntry.email has been persisted and read " + "correctly", mSharedPreferenceEntry.getEmail(), is(equalTo(savedSharedPreferenceEntry.getEmail()))); } “Wall of Code” You have to read comments, not code Testing too much at once Failure messages adding noise to test itself

Slide 16

Slide 16 text

public boolean savePersonalInfo(SharedPreferenceEntry sharedPreferenceEntry){ // Start a SharedPreferences transaction. SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.putString(KEY_NAME, sharedPreferenceEntry.getName()); editor.putLong(KEY_DOB, sharedPreferenceEntry.getDateOfBirth().getTimeInMillis()); editor.putString(KEY_EMAIL, sharedPreferenceEntry.getName()); // Commit changes to SharedPreferences. return editor.commit(); } Mistakes...

Slide 17

Slide 17 text

# ./gradlew unit:BasicSample:app:testDebug com.example.android.testing.unittesting.BasicSample.SharedPreferencesHelperTest > sharedPreferencesHelper_SavePersonalInformationFailed_ReturnsFalse PASSED com.example.android.testing.unittesting.BasicSample.SharedPreferencesHelperTest > sharedPreferencesHelper_SaveAndReadPersonalInformation PASSED BUILD SUCCESSFUL Run the Tests!

Slide 18

Slide 18 text

WTF?

Slide 19

Slide 19 text

If the test fails, and you can't immediately tell why... The test is bad, unhelpful. If you can mess up the code under test, and the test DOESN'T fail... The test is actively harmful. Unhelpful and Harmful Tests

Slide 20

Slide 20 text

private SharedPreferencesHelper createMockSharedPreference() { // Mocking reading the SharedPreferences as if mMockSharedPreferences was previously written // correctly. when(mMockSharedPreferences.getString(eq(SharedPreferencesHelper.KEY_NAME), anyString())) .thenReturn(mSharedPreferenceEntry.getName()); when(mMockSharedPreferences.getString(eq(SharedPreferencesHelper.KEY_EMAIL), anyString())) .thenReturn(mSharedPreferenceEntry.getEmail()); when(mMockSharedPreferences.getLong(eq(SharedPreferencesHelper.KEY_DOB), anyLong())) .thenReturn(mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis()); // Mocking a successful commit. when(mMockEditor.commit()).thenReturn(true); // Return the MockEditor when requesting it. when(mMockSharedPreferences.edit()).thenReturn(mMockEditor); return new SharedPreferencesHelper(mMockSharedPreferences); } Why Does it Fail?

Slide 21

Slide 21 text

@Test public void savesPersonalInformation() { // Given sharedPrefsSaveWillSucceed(); // When boolean saveSuccess = sharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); // Then verify(mMockEditor).putString(KEY_NAME, mSharedPreferenceEntry.getName()); verify(mMockEditor).putString(KEY_EMAIL, mSharedPreferenceEntry.getEmail()); verify(mMockEditor).putLong(KEY_DOB, mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis()); verify(mMockEditor).commit(); assertThat(saveSuccess, is(true)); } @Test public void readsPersonalInformation() { // Given givenSharedPrefsContains(mSharedPreferenceEntry); // When SharedPreferenceEntry retrievedInfo = sharedPreferencesHelper.getPersonalInfo(); // Then assertThat(retrievedInfo.getName(), is(equalTo(mSharedPreferenceEntry.getName()))); assertThat(retrievedInfo.getDateOfBirth(), is(equalTo(mSharedPreferenceEntry.getDateOfBirth()))); assertThat(retrievedInfo.getEmail(), is(equalTo(mSharedPreferenceEntry.getEmail()))); } Split the Test, Check What Matters

Slide 22

Slide 22 text

# ./gradlew unit:BasicSample:app:testDebug com.example.android.testing.unittesting.BasicSample.SharedPreferencesHelperTest > savesPersonalInformation FAILED org.mockito.exceptions.verification.junit.ArgumentsAreDifferent at SharedPreferencesHelperTest.java:94 # ./gradlew unit:BasicSample:app:testDebug --info com.example.android.testing.unittesting.BasicSample.SharedPreferencesHelperTest > savesPersonalInformation FAILED Argument(s) are different! Wanted: mMockEditor.putString( "key_email", "[email protected]" ); -> at com.example.android.testing.unittesting.BasicSample.SharedPreferencesHelperTest.savesPersonalInformation(Sh aredPreferencesHelperTest.java:94) Actual invocation has different arguments: mMockEditor.putString( "key_email", "Test name" ); -> at ... Run the Tests!

Slide 23

Slide 23 text

@Test public void savesPersonalInformation() { // Given sharedPrefsSaveWillSucceed(); // When boolean saveSuccess = sharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); // Then verify(mMockEditor).putString(KEY_NAME, mSharedPreferenceEntry.getName()); verify(mMockEditor).putString(KEY_EMAIL, mSharedPreferenceEntry.getEmail()); verify(mMockEditor).putLong(KEY_DOB, mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis()); verify(mMockEditor).commit(); assertThat(saveSuccess, is(true)); } The Save Test

Slide 24

Slide 24 text

@Test public void reportsSuccessSavingPersonalInformation() { // Given sharedPrefsSaveWillSucceed(); // When boolean saveSuccess = sharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); // Then assertThat(saveSuccess, is(true)); } @Test public void savesPersonalInformation() { // Given sharedPrefsSaveWillSucceed(); // When sharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); // Then verify(mMockEditor).putString(KEY_NAME, mSharedPreferenceEntry.getName()); verify(mMockEditor).putString(KEY_EMAIL, mSharedPreferenceEntry.getEmail()); verify(mMockEditor).putLong(KEY_DOB, mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis()); verify(mMockEditor).commit(); } Split It!

Slide 25

Slide 25 text

@Test public void savesName() { // When sharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); // Then verify(mMockEditor).putString(KEY_NAME, mSharedPreferenceEntry.getName()); verify(mMockEditor).commit(); } @Test public void savesEmail() { // When sharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); // Then verify(mMockEditor).putString(KEY_EMAIL, mSharedPreferenceEntry.getEmail()); verify(mMockEditor).commit(); } @Test public void savesDateOfBirth() { // When sharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); // Then verify(mMockEditor).putLong(KEY_DOB, mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis()); verify(mMockEditor).commit(); } Split... MORE!

Slide 26

Slide 26 text

@Test public void savesName() { // When sharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry); // Then verifySavedAndCommitted(KEY_NAME, mSharedPreferenceEntry.getName()); } @Test public void savesEmail() { ... verifySavedAndCommitted(KEY_EMAIL, mSharedPreferenceEntry.getEmail()); } @Test public void savesDateOfBirth() { ... verifySavedAndCommitted(KEY_DOB, mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis()); } private void verifySavedAndCommitted(final String keyName, final String value) { verify(mMockEditor).putString(keyName, value); verify(mMockEditor).commit(); } private void verifySavedAndCommitted(String keyName, long value) { ... } Less Duplication, More Readable

Slide 27

Slide 27 text

@Test public void readsPersonalInformation() { // Given givenSharedPrefsContains(mSharedPreferenceEntry); // When SharedPreferenceEntry retrievedInfo = sharedPreferencesHelper.getPersonalInfo(); // Then assertThat(retrievedInfo.getName(), is(equalTo(mSharedPreferenceEntry.getName()))); assertThat(retrievedInfo.getDateOfBirth(), is(equalTo(mSharedPreferenceEntry.getDateOfBirth()))); assertThat(retrievedInfo.getEmail(), is(equalTo(mSharedPreferenceEntry.getEmail()))); } The Read Test

Slide 28

Slide 28 text

@Test public void readsPersonalInformation() { // Given givenSharedPrefsContains(mSharedPreferenceEntry); // When SharedPreferenceEntry retrievedPersonalInfo = sharedPreferencesHelper.getPersonalInfo(); // Then assertThat(retrievedPersonalInfo, matchesEntry(mSharedPreferenceEntry)); } Single Assertion (Hamcrest Matchers)

Slide 29

Slide 29 text

public class SharedPreferenceEntryMatchers { public static Matcher matchesEntry(final SharedPreferenceEntry entry) { return allOf( matchesNameOf(entry), matchesEmailOf(entry), matchesDateOfBirthOf(entry) ); } private static FeatureMatcher matchesNameOf(final SharedPreferenceEntry entry) { return new FeatureMatcher(equalTo(entry.getName()), "name", "name") { @Override protected String featureValueOf(final SharedPreferenceEntry sharedPreferenceEntry) { return sharedPreferenceEntry.getName(); } }; } ... } Matchers Caveat...

Slide 30

Slide 30 text

● Natural, expressive; read many x more than write. ● SNR ● Assert / check / test one thing. Matchers can help. ● Test boundaries, don't poke internals (package scope included) Should be able to change implementation and tests still pass. Should NOT be able to BREAK implementation and still have tests pass. Recap: Top Testing Tips

Slide 31

Slide 31 text

● Code modification >>> creation → save time each time test fails, is looked at. ● Unit tests lowest level → failure must be clear why and where. ● Good failures: ● test name ● message, info given ● test one thing Failure

Slide 32

Slide 32 text

● Spock Framework ● https:/ /github.com/spockframework/spock ● http:/ /spockframework.org/ ● No logo yet... ● Decent documentation... on Google Code... Going Further... Logically IMAGINE A FANTASTIC LOGO HERE!

Slide 33

Slide 33 text

def "subscribers receive published events at least once"() { when: publisher.send(event) then: (1.._) * subscriber.receive(event) where: event << ["started", "paused", "stopped"] } def "can cope with misbehaving subscribers"() { given: firstSubscriber.receive(_) >> { throw new Exception() } when: publisher.send("event1") publisher.send("event2") then: 1 * latterSubscriber.receive("event1") 1 * latterSubscriber.receive("event2") } Some Examples

Slide 34

Slide 34 text

def "reads personal info"() { given: sharedPrefsContains(entry) when: def retrievedEntry = sharedPreferencesHelper.getPersonalInfo() then: expect retrievedEntry, matchesEntry(entry) } Spock Hamcrest Support

Slide 35

Slide 35 text

def "reads personal info"() { given: mockSharedPrefs.has(expectedEntry) // extension method when: def entry = sharedPreferencesHelper.getPersonalInfo() then: resulting entry matches expectedEntry // resulting(entry).matches(expectedEntry) } Groovy DSL (+ Metaprogramming) Magic

Slide 36

Slide 36 text

com.example.android.testing.unittesting.BasicSample.SharedPreferencesHelperSpec > reads personal info FAILED Condition not satisfied: val.email == expected.email | | | | | | | | | [email protected] | | | com.example.android.testing.unittesting.BasicSample.SharedPreferenceEntry@71ea020a | | false | | 9 differences (62% similarity) | | [email protected](_AN_ERROR) | | [email protected](---------) | [email protected]_AN_ERROR com.example.android.testing.unittesting.BasicSample.SharedPreferenceEntry@1e6abe27 at com.example..SharedPreferencesHelperSpec.resulting_closure2(SharedPreferencesHelperSpec.groovy:122) at com.example..SharedPreferencesHelperSpec.reads personal info(SharedPreferencesHelperSpec.groovy:87) With Amazing Failure Info!

Slide 37

Slide 37 text

def "uses spy"() { when: thing.doSomethingWith(collaborator) then: 1 * collaborator.callWeExpect() } def "uses stub"() { given: stubRandomNumberGenerator.getValue() >> 4 // chosen at random when: def result = thing.doThingWith(stubRandomNumberGenerator) then: result == 42 } Wildcards and ranges: _.receive(event) // receive is called on any mock object 0 * _._ // No more interactions 0 * _ // Same thing 3 * subscriber.receive(event) // exactly 3 times (1.._) * subscriber.receive(event) // at least 1 time (_..2) * subscriber.receive(event) // at most 2 times 0 * _./set.*/(_) // no setter is called on any mock Mocking, Stubbing, Spying

Slide 38

Slide 38 text

pickySubscriber.receive(_) >> { Event event -> event.priority > 3 ? "ok" : "fail" } badSubscriber.receive(_) >> { throw new InternalError("I'm a troublemaker!") } subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok" 1 * subscriber.receive(!null) // arg must not be null 1 * subscriber.receive(!unwantedEvent) // arg must not be equal to unwantedEvent 1 * subscriber.receive( { it.priority >= 5 } ) // arg must have priority 5 or above More: https:/ /spockframework.github.io/spock/docs/1.0/interaction_based_testing.html More Fakery, Argument Constraints

Slide 39

Slide 39 text

@Unroll def "maximum of #a and #b is #result"() { expect: Math.max(a, b) == result where: a | b || result 0 | 0 || 0 0 | 9 || 9 -2 | 1 || 3 } ....SharedPreferencesHelperSpec > maximum of -2 and 1 is 3 FAILED Condition not satisfied: Math.max(a, b) == result | | | | | 1 -2 1 | 3 false at com.example..MyClassSpec.maximum of #a and #b is #result(SharedPreferencesHelperSpec.groovy:93) Data Driven Tests

Slide 40

Slide 40 text

when: thing.addValue(DUMMY_VALUE) then: thing.previousValue == old(thing.newestValue) Goddamn magic How? What? Huh?

Slide 41

Slide 41 text

● Groovy-based ● “Kind of Java” ● Expressive DSL abilities (http:/ /docs.groovy-lang.org/docs/latest/html/documentation/core-domain-specific-languages.html) ● Freakin' Magic ● Built-in expressive mocking ● Readable, unrolling, data-driven tests (https:/ /spockframework.github.io/spock/docs/1.0/data_driven_testing.html) Spock Features

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

● Similar to unit tests, but wider scope ● Test domain model satisfies business logic ● May test interaction of many units. Focused on use cases instead. ● Still off-device (fast), isolate domain from platform Domain / “Service” / API Tests

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

● Why? ● Challenges ● Tips UI Tests

Slide 46

Slide 46 text

● Obvious business value, on complete product ● Test UI (shallow) ● Test core use cases, main interactions (deep) ● Test specific bugs (fix, prevent regressions) ● Link to BDD, conversation → spec BUT: ● Can end up with too many, taking too long and breaking too often Why We Write UI Tests

Slide 47

Slide 47 text

● Speed (lack of) ● Synchronisation issues ● Unclear failure causes ● Brittleness ● Readability UI Test Challenges

Slide 48

Slide 48 text

● Avoid Thread.sleep(millis), -> Espresso synch & IdlingResource ● Developer machine: ● Genymotion, x86(_64) > ARM ● Many emulators & test sharding (e.g. Fork by Shazam) ● On CI: ● Many devices (also sharding) Fixes – Slow / Synchronisation Issues

Slide 49

Slide 49 text

Custom failure handler: BetterFailureHandler failureHandler = new BetterFailureHandler(); Espresso.setFailureHandler(failureHandler); Spoon screenshot: Spoon.screenshot(activity, sanitise(tag)); UiDevice screenshot: final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); final Context targetContext = InstrumentationRegistry.getTargetContext(); final UiDevice uiDevice = UiDevice.getInstance(instrumentation); final File screenshotFile = new File(testScreenshotsDir, screenshotName); uiDevice.takeScreenshot(screenshotFile); Fixes – Unclear Failure Causes

Slide 50

Slide 50 text

● Slow ● Synchronisation issues ● Unclear failure reason ● Readability ● Brittleness ● Flakiness / reproducibility UI Test Challenges

Slide 51

Slide 51 text

@Test public void changeText() { // Type text and then press the button. onView(withId(R.id.editTextUserInput)) .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard()); onView(withId(R.id.changeTextBt)).perform(click()); // Check that the text was changed. onView(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED))); } Readability: A Bitter Little Espresso Comments because code isn't readable enough Overly verbose, backward English Could do with a better name

Slide 52

Slide 52 text

@Test public void submittingTextChangesTextDisplayed() { type(DUMMY_STRING).into(inputTextField()).then(closeSoftKeyboard()); clickOn(submitButton()); checkThat(displayText(), hasText(STRING_TO_BE_TYPED)); } Work In Progress, please contribute! https:/ /github.com/jonreeve/espresso-sugar Readability: Adding Some Sugar

Slide 53

Slide 53 text

● android-spock ● https:/ /github.com/pieces029/android-spock ● Runs on device ● Uses Spock, Groovy ● Trade-off: ● DSL gets better! ● Auto-completion gets worse... (at least for now) Readability: Spock, Again!

Slide 54

Slide 54 text

@Test public void showsWelcomeOnFirstLogin() { type(USERNAME).into(view(withId(R.id.username_text))); type(PASSWORD).into(view(withId(R.id.password_text))); clickOn(view(withId(R.id.login_button))); checkThat(view(withId(R.id.welcome)), isDisplayed()); } OR... @Test public void showsWelcomeOnFirstLogin() { final MainScreen mainScreen = onLoginScreen().loginAs(FIRST_TIME_USER); checkThat(mainScreen.welcomeMessage(), isDisplayed()) } Readability + Brittleness: Page / Screen Models

Slide 55

Slide 55 text

@Test public void showsWelcomeOnFirstLogin() { when(FIRST_TIME_USER).logsIn(); then(welcomeMessage(), isDisplayed()) } Also see Gwen, by Shazam: https:/ /github.com/shazam/gwen Readability + Brittleness: Actor Models, Other

Slide 56

Slide 56 text

"Hermetic" test environment Magical, mystical. Astrology, Alchemy... Completely sealed off (as if by magic) Identify sources of unpredictability, and Fake them (wrappers, mocking, hexagonal architecture, etc.) Flakiness / Reliability

Slide 57

Slide 57 text

No such thing! But... ● Is it worth it? ● Is there a better level to test it at? ● Is there a better place to introduce a seam and isolate? ● Pairing and rubber-ducking helps maintain perspective Untestable?

Slide 58

Slide 58 text

● Speed: Espresso, many emulators / devices + sharding (Fork) ● Make tests readable! Page / actor models, DSL, android-spock ● Make failures clear, add useful info + screenshots ● Flakiness: Define boundaries, fake the unpredictable! ● Hard to test? Best way to test? Pairing, rubber-ducking, etc. Recap: UI Testing Tips

Slide 59

Slide 59 text

Jon Reeve Wasabi Code @themightyjon Thank you! droidcon Greece Thessaloniki