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
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
Hexagonal Architecture / Ports & Adapters / “Clean Architecture” • Rule out & wall off “hard to test” • ... Basically, separation of concerns, divide by layers. How To Overcome?
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
“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
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
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
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?
# ./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!
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
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
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
} 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
"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
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
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
(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
machine: • Genymotion, x86(_64) > ARM • Many emulators & test sharding (e.g. Fork by Shazam) • On CI: • Many devices (also sharding) Fixes – Slow / Synchronisation Issues
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
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?
• 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