● 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
● 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
● 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
● 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
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
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
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?
@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
@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!
@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
@Test public void readsPersonalInformation() { // Given givenSharedPrefsContains(mSharedPreferenceEntry); // When SharedPreferenceEntry retrievedPersonalInfo = sharedPreferencesHelper.getPersonalInfo(); // Then assertThat(retrievedPersonalInfo, matchesEntry(mSharedPreferenceEntry)); } Single Assertion (Hamcrest Matchers)
● 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
● 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
● 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!
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
@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
● 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
● 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
@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
@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
"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
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?
● 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