Android Testing
Unit Testing Small scoped, isolated
public void maximizeVolume(AudioManager audioManager) { if (audioManager.getRingerMode() != RINGER_MODE_SILENT) {
int max = audioManager.getStreamMaxVolume(STREAM_RING); audioManager.setStreamVolume(STREAM_RING, max, 0); } }
JUnit http://junit.org/
public void testNormalRingerIsMaximized() { audioManager.setRingerMode(RINGER_MODE_NORMAL); maximizeVolume(audioManager); assertEquals(audioManager.getStreamVolume(STREAM_RING), max); } !
public void testSilentRingerIsNotDisturbed() { audioManager.setRingerMode(RINGER_MODE_SILENT); maximizeVolume(audioManager); assertEquals(audioManager.getStreamVolume(STREAM_RING), 0); }
java.lang.RuntimeException: Stub!
public void setRingerMode(int mode) { throw new RuntimeException("Stub!"); }
Can’t Test Outside A Device!
Can’t Test Outside A Device!
^ Can’t Test Outside A Device! Wouldn’t Want To
Tests Only Non-Android API's
Android Testing Framework https://developer.android.com/tools/testing/ testing_android.html
public void testNormalRingerIsMaximized() { audioManager.setRingerMode(RINGER_MODE_NORMAL); maximizeVolume(audioManager); assertEquals(audioManager.getStreamVolume(STREAM_RING), max); } !
public void testSilentRingerIsNotDisturbed() { audioManager.setRingerMode(RINGER_MODE_SILENT); maximizeVolume(audioManager); assertEquals(audioManager.getStreamVolume(STREAM_RING), 0); }
Tests Real Android API’s
Run on Real Devices
Slow!
Fast Keep ‘em
Robolectric http://robolectric.org/
@RunWith(RobolectricTestRunner.class) public void testNormalRingerIsMaximized() { audioManager.setRingerMode(RINGER_MODE_NORMAL); maximizeVolume(audioManager); assertEquals(audioManager.getStreamVolume(STREAM_RING), max); }
! @RunWith(RobolectricTestRunner.class) public void testSilentRingerIsNotDisturbed() { audioManager.setRingerMode(RINGER_MODE_SILENT); maximizeVolume(audioManager); assertEquals(audioManager.getStreamVolume(STREAM_RING), 0); }
@RunWith(RobolectricTestRunner.class) public void testNormalRingerIsMaximized() { audioManager.setRingerMode(RINGER_MODE_NORMAL); maximizeVolume(audioManager); assertEquals(audioManager.getStreamVolume(STREAM_RING), max); }
! @RunWith(RobolectricTestRunner.class) public void testSilentRingerIsNotDisturbed() { audioManager.setRingerMode(RINGER_MODE_SILENT); maximizeVolume(audioManager); assertEquals(audioManager.getStreamVolume(STREAM_RING), 0); }
Tests Fail Occasionally.
expected: <0> but was: <30>
?
Concise Keep ‘em
fest-android http://square.github.io/fest-android/
@RunWith(RobolectricTestRunner.class) public void testNormalRingerIsMaximized() { audioManager.setRingerMode(RINGER_MODE_NORMAL); maximizeVolume(audioManager); assertThat(audioManager).hasStreamVolume(STREAM_RING, max); }
! @RunWith(RobolectricTestRunner.class) public void testSilentRingerIsNotDisturbed() { audioManager.setRingerMode(RINGER_MODE_SILENT); maximizeVolume(audioManager); assertThat(audioManager).hasStreamVolume(STREAM_RING, 0); }
@RunWith(RobolectricTestRunner.class) public void testNormalRingerIsMaximized() { audioManager.setRingerMode(RINGER_MODE_NORMAL); maximizeVolume(audioManager); assertThat(audioManager).hasStreamVolume(STREAM_RING, max); }
! @RunWith(RobolectricTestRunner.class) public void testSilentRingerIsNotDisturbed() { audioManager.setRingerMode(RINGER_MODE_SILENT); maximizeVolume(audioManager); assertThat(audioManager).hasStreamVolume(STREAM_RING, 0); }
Expected ring stream volume <0> but was <30>.
Reliable Keep ‘em
Mockito https://code.google.com/p/mockito/
public void testSilentRingerIsNotDisturbed() { // Prepare mocks and script their
behavior. AudioManager audioManager = Mockito.mock(AudioManager.class); Mockito.when(audioManager.getRingerMode()) .thenReturn(RINGER_MODE_SILENT); ! // Test the code of interest. maximizeVolume(audioManager); ! // Validate that we saw exactly what we wanted. Mockito.verify(audioManager).getRingerMode(); Mockito.verifyNoMoreInteractions(audioManager); }
public void testNormalRingerIsMaximized() { // Prepare mocks and script their
behavior. AudioManager audioManager = mock(AudioManager.class); when(audioManager.getRingerMode()) .thenReturn(RINGER_MODE_NORMAL); when(audioManager.getStreamMaxVolume(STREAM_RING)) .thenReturn(100); ! // Test the code of interest. maximizeVolume(audioManager); ! // Validate that we saw exactly what we wanted. verify(audioManager) .setStreamVolume(STREAM_RING, 100, 0); }
Verify Interactions Also Great for Integration Testing!
Integration Testing Larger scoped, verify interactions
Monkey https://developer.android.com/tools/help/ monkey.html
Not MonkeyRunner! https://developer.android.com/tools/help/ monkeyrunner_concepts.html
adb shell monkey -p your.package.name -v 500
Low Cost
Simple
Useless* *well, almost
Instrumentation API https://developer.android.com/reference/android/app/ Instrumentation.html
public void testPasswordTooShort_ShowsError() { // Make sure the initial state
does not show any errors. assertThat(username).hasNoError(); assertThat(password).hasNoError(); instrumentation.runOnMainSync(new Runnable() { @Override public void run() { // Type a value into the username and password fields. username.setText("prateek"); password.setText(“password”); // Click the "login" button. login.performClick(); } }); instrumentation.waitForIdleSync(); ! // Verify error was shown only for username field. assertThat(username).hasNoError(); assertThat(password).hasError(R.string.password_length); }
Much Boilerplate!
Robotium https://code.google.com/p/robotium/
public void testPasswordTooShort_ShowsError() { // Make sure the initial state
does not show any errors. assertThat(username).hasNoError(); assertThat(password).hasNoError(); ! // Type a value into the username and password fields. solo.typeText(username, “prateek”); solo.typeText(password, “password”); // Click the "login" button. solo.clickOnView(login); ! // Verify error was shown only for username field. assertThat(username).hasNoError(); assertThat(password).hasError(R.string.password_length); }
Espresso https://code.google.com/p/android-test-kit/wiki/ Espresso
Double Espresso https://github.com/JakeWharton/double-espresso
public void testPasswordTooShort_ShowsError() { // Make sure the initial state
does not show any errors. assertThat(username).hasNoError(); assertThat(password).hasNoError(); ! // Type a value into the username and password fields. onView(withId(R.id.username)).perform(typeText("prateek")); onView(withId(R.id.password)).perform(typeText("password")); onView(withId(R.id.login)).perform(click()); ! // Verify error was shown only for username field. assertThat(username).hasNoError(); assertThat(password).hasError(R.string.password_length); }
Spoon http://square.github.io/spoon/
public void testPasswordTooShort_ShowsError() { Spoon.screenshot(activity, "initial_state"); assertThat(username).hasNoError(); assertThat(password).hasNoError(); ! onView(withId(R.id.username)).perform(typeText("prateek"));
onView(withId(R.id.password)).perform(typeText("password")); Spoon.screenshot(activity, "values_entered"); onView(withId(R.id.login)).perform(click()); Spoon.screenshot(activity, "login_clicked"); ! assertThat(username).hasNoError(); assertThat(password).hasError(R.string.password_length); }
public void testPasswordTooShort_ShowsError() { Spoon.screenshot(activity, "initial_state"); assertThat(username).hasNoError(); assertThat(password).hasNoError(); ! onView(withId(R.id.username)).perform(typeText("prateek"));
onView(withId(R.id.password)).perform(typeText("password")); Spoon.screenshot(activity, "values_entered"); onView(withId(R.id.login)).perform(click()); Spoon.screenshot(activity, "login_clicked"); ! assertThat(username).hasNoError(); assertThat(password).hasError(R.string.password_length); }