Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Testing 2.0

Android Testing 2.0

Build a toolset to write rock solid apps.

Prateek Srivastava

April 10, 2015
Tweet

More Decks by Prateek Srivastava

Other Decks in Technology

Transcript

  1. Android Testing

    View Slide

  2. Unit Tests
    small scoped and isolated

    View Slide

  3. public static boolean isNullOrEmpty(String text) {
    return TextUtils.isEmpty(text) || TextUtils.getTrimmedLength(text) == 0;
    }

    View Slide

  4. JUnit
    http://junit.org

    View Slide

  5. public class UtilsTest extends TestCase {
    public void testIsNullOrEmpty() {
    assertEquals(isNullOrEmpty(null), true);
    assertEquals(isNullOrEmpty(""), true);
    assertEquals(isNullOrEmpty(" "), true);
    assertEquals(isNullOrEmpty("foo"), false);
    }
    }

    View Slide

  6. public class UtilsTest {
    @Test public void isNullOrEmpty() {
    assertEquals(isNullOrEmpty(null), true);
    assertEquals(isNullOrEmpty(""), true);
    assertEquals(isNullOrEmpty(" "), true);
    assertEquals(isNullOrEmpty("foo"), false);
    }
    }

    View Slide

  7. java.lang.RuntimeException: Stub!

    View Slide

  8. public class TextUtils {
    ...
    public static boolean isEmpty(CharSequence cs) {
    throw new RuntimeException("Stub!");
    }
    public static int getTrimmedLength(CharSequence cs) {
    throw new RuntimeException("Stub!");
    }
    }

    View Slide

  9. Android Testing
    Framework
    https://developer.android.com/tools/testing/
    testing_android.html

    View Slide

  10. public class UtilsTest extends TestCase {
    public void testIsNullOrEmpty() {
    assertEquals(isNullOrEmpty(null), true);
    assertEquals(isNullOrEmpty(""), true);
    assertEquals(isNullOrEmpty(" "), true);
    assertEquals(isNullOrEmpty("foo"), false);
    }
    }
    http://developer.android.com/reference/junit/framework/
    TestCase.html

    View Slide

  11. Slow!

    View Slide

  12. Can’t Test Outside Device

    View Slide

  13. Can’t Test Outside Device

    View Slide

  14. Java Module

    View Slide

  15. public static boolean isNullOrEmpty(String text) {
    return text == null || text.trim().length() == 0;
    }

    View Slide

  16. View Slide

  17. settings.gradle
    include 'library'
    include 'app'
    app/build.gradle
    dependencies {
    compile(':library')
    }

    View Slide

  18. Robolectric
    http://robolectric.org/

    View Slide

  19. @RunWith(RobolectricTestRunner.class)
    public class UtilsTest {
    @Test public void isNullOrEmpty() {
    assertEquals(isNullOrEmpty(null), true);
    assertEquals(isNullOrEmpty(""), true);
    assertEquals(isNullOrEmpty(" "), true);
    assertEquals(isNullOrEmpty("foo"), false);
    }
    }

    View Slide

  20. @RunWith(RobolectricTestRunner.class)
    public class UtilsTest {
    @Test public void isNullOrEmpty() {
    assertEquals(isNullOrEmpty(null), true);
    assertEquals(isNullOrEmpty(""), true);
    assertEquals(isNullOrEmpty(" "), true);
    assertEquals(isNullOrEmpty("foo"), false);
    }
    }

    View Slide

  21. Robolectric + Gradle
    https://github.com/segmentio/analytics-android/blob/2.5.1/core-
    tests/build.gradle#L46

    View Slide

  22. Instrumentation Tests
    larger scoped, black box testing
    Espresso, Spoon, Wiremock, Oh my! - Michael Bailey
    https://www.youtube.com/watch?v=-xQCNf_5NNM

    View Slide

  23. public class LoginActivity extends Activity {
    @InjectView(R.id.username) EditText username;
    @InjectView(R.id.password) EditText password;
    @OnClick(R.id.login) void onLoginClicked() {
    boolean hasError = false;
    if (TextUtils.isEmpty(username.getText())) {
    username.setError(getString(R.string.required));
    hasError = true;
    } else {
    username.setError(null);
    }
    if (TextUtils.isEmpty(password.getText())) {
    password.setError(getString(R.string.required));
    hasError = true;
    } else {
    password.setError(null);
    }
    if (!hasError) {
    Intent intent = new Intent(LoginActivity.this, OrderActivity.class);
    intent.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
    startActivity(intent);
    finish();
    }
    }
    }

    View Slide

  24. Instrumentation API
    https://developer.android.com/reference/android/app/
    Instrumentation.html

    View Slide

  25. public void testEmptyPassword_ShowsError() {
    // Make sure the initial state does not show any errors.
    assertEquals(username.getError(), null);
    assertEquals(password.getError(), null);
    instrumentation.runOnMainSync(new Runnable() {
    @Override public void run() {
    // Type a value into the username and password fields.
    username.setText("elvis");
    password.setText("");
    // Click the "login" button.
    login.performClick();
    }
    });
    instrumentation.waitForIdleSync();
    // Verify error was shown only for password field.
    assertEquals(username.getError(), null);
    assertEquals(password.getError(), getString(R.string.required));
    }

    View Slide

  26. public void testEmptyPassword_ShowsError() {
    // Make sure the initial state does not show any errors.
    assertEquals(username.getError(), null);
    assertEquals(password.getError(), null);
    instrumentation.runOnMainSync(new Runnable() {
    @Override public void run() {
    // Type a value into the username and password fields.
    username.setText("elvis");
    password.setText("");
    // Click the "login" button.
    login.performClick();
    }
    });
    instrumentation.waitForIdleSync();
    // Verify error was shown only for password field.
    assertEquals(username.getError(), null);
    assertEquals(password.getError(), getString(R.string.required));
    }

    View Slide

  27. assertEquals(password.getError(), null);
    assertEquals(password.getVisibility(), VISIBLE);
    assertEquals(password.getText(), null);
    assertEquals(password.getHint(), getString(R.string.password));

    View Slide

  28. expected: <8> but was: <4>

    View Slide

  29. AssertJ-Android
    http://square.github.io/assertj-android/

    View Slide

  30. assertThat(username)
    .hasNoError()
    .isVisible()
    .isEmpty()
    .hasHint(R.string.hint);

    View Slide

  31. Expected visibility but was .

    View Slide

  32. Robotium
    https://code.google.com/p/robotium/

    View Slide

  33. public void testEmptyPassword_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, "elvis");
    solo.typeText(password, "");
    // Click the "login" button.
    solo.clickOnView(login);
    // Verify error was shown only for username field.
    assertThat(username).hasNoError();
    assertThat(password).hasError(R.string.required);
    }

    View Slide

  34. Espresso
    https://code.google.com/p/android-test-kit/

    View Slide

  35. public void testEmptyPassword_ShowsError() {
    // Make sure the initial state does not show any errors.
    onView(withId(R.id.username)).check(matches(hasNoError()));
    onView(withId(R.id.password)).check(matches(hasNoError()));
    // Type a value into the username and password fields.
    onView(withId(R.id.username)).perform(typeText("elvis"));
    onView(withId(R.id.password)).perform(typeText(""));
    // Click the "login" button.
    onView(withId(R.id.login)).perform(click());
    // Verify error was shown only for username field.
    onView(withId(R.id.password)).check(matches(hasNoError()));
    onView(withId(R.id.password))
    .check(matches(hasError(R.string.required)));
    }
    https://code.google.com/p/android-test-kit/wiki/
    EspressoV2CheatSheet

    View Slide

  36. Spoon
    https://github.com/square/spoon
    http://square.github.io/spoon/sample/index.html

    View Slide

  37. View Slide

  38. public void testEmptyPassword_ShowsError() {
    Spoon.screenshot(activity, "initial_state");
    // Make sure the initial state does not show any errors.
    onView(withId(R.id.username)).check(matches(hasNoError()));
    onView(withId(R.id.password)).check(matches(hasNoError()));
    // Type a value into the username and password fields.
    onView(withId(R.id.username)).perform(typeText("elvis"));
    onView(withId(R.id.password)).perform(typeText(""));
    Spoon.screenshot(activity, "typed_text");
    // Click the "login" button.
    onView(withId(R.id.login)).perform(click());
    Spoon.screenshot(activity, "login_clicked");
    // Verify error was shown only for username field.
    onView(withId(R.id.password)).check(matches(hasNoError()));
    onView(withId(R.id.password))
    .check(matches(hasError(R.string.required)));
    }

    View Slide

  39. public void testEmptyPassword_ShowsError() {
    Spoon.screenshot(activity, "initial_state");
    // Make sure the initial state does not show any errors.
    onView(withId(R.id.username)).check(matches(hasNoError()));
    onView(withId(R.id.password)).check(matches(hasNoError()));
    // Type a value into the username and password fields.
    onView(withId(R.id.username)).perform(typeText("elvis"));
    onView(withId(R.id.password)).perform(typeText(""));
    Spoon.screenshot(activity, "typed_text");
    // Click the "login" button.
    onView(withId(R.id.login)).perform(click());
    Spoon.screenshot(activity, "login_clicked");
    // Verify error was shown only for username field.
    onView(withId(R.id.password)).check(matches(hasNoError()));
    onView(withId(R.id.password))
    .check(matches(hasError(R.string.required)));
    }

    View Slide

  40. View Slide

  41. Fork
    https://github.com/shazam/fork

    View Slide

  42. Mocks
    script responses, verify interactions
    http://martinfowler.com/articles/mocksArentStubs.html

    View Slide

  43. Mockito
    http://mockito.org
    https://corner.squareup.com/2012/10/mockito-android.html

    View Slide

  44. analytics.identify("elvis", new Traits()
    .putFirstName("Elvis")
    .putLastName("Presley")
    .putEmail("[email protected]"));

    View Slide

  45. class MixpanelAdapter implements Adapter {
    final MixpanelAPI.People people;

    void identify(IdentifyMessage message) {
    JSONObject json = new JSONObject();
    json.put("$email", message.email());
    json.put("$first_name", message.firstName());
    json.put("$last_name", message.lastName());
    people.identify(message.userId());
    people.set(json);
    }
    }

    View Slide

  46. @Test public void identify() throws JSONException {
    // 1. Prepare our mocks
    MixpanelAPI.People people = Mockito.mock(MixpanelAPI.People.class);
    MixpanelAdapter adapter = mockAdapter(people);
    Analytics analytics = createAnalyticsWithMock(adapter);
    // 2. Exercise the test
    analytics.identify("elvis", new Traits()
    .putFirstName("Elvis")
    .putLastName("Presley")
    .putEmail("[email protected]"));
    JSONObject expected = new JSONObject();
    expected.put("userId", "elvis");
    expected.put("$email", "[email protected]");
    expected.put("$first_name", "Elvis");
    expected.put("$last_name", "Presley");
    // 3. Verify that we saw what we expected
    Mockito.verify(people).identify("elvis");
    Mockito.verify(people).set(eq(expected));
    }

    View Slide

  47. PowerMockito
    https://code.google.com/p/powermock/

    View Slide

  48. class LocalyticsAdapter implements Adapter {
    void identify(IdentifyMessage message) {
    Localytics.setCustomerId(message.userId());
    Localytics.setIdentifier("email", message.email());
    Localytics.setIdentifier("customer_name", message.name());
    }
    }

    View Slide

  49. @Test public void identify() {
    // 1. Prepare mocks.
    PowerMockito.mockStatic(Localytics.class);
    Analytics analytics = createAnalytics();
    // 2. Test the code of interest.
    analytics.identify("elvis", new Traits()
    .putFirstName("Elvis")
    .putLastName("Presley")
    .putEmail("[email protected]"));
    // 3. Validate that we saw exactly what we wanted.
    PowerMockito.verifyStatic();
    Localytics.setCustomerId("elvis");
    PowerMockito.verifyStatic();
    Localytics.setIdentifier("email", "[email protected]");
    PowerMockito.verifyStatic();
    Localytics.setIdentifier("customer_name", "Elvis Presley");
    }

    View Slide

  50. MockWebServer
    https://github.com/square/okhttp/tree/master/mockwebserver

    View Slide

  51. public void test() throws Exception {
    // Create a MockWebServer.
    MockWebServer server = new MockWebServer();
    // Schedule some responses.
    server.enqueue(new MockResponse().setBody("hello, world!"));
    // Start the server.
    server.start();
    // Ask the server for its URL. You'll need this to make HTTP requests.
    URL baseUrl = server.getUrl(“/v1/chat/“);
    // Exercise your application code, which makes those HTTP requests.
    // Responses are returned in the same order that they are enqueued.
    Chat chat = new Chat(baseUrl);
    chat.loadMore();
    assertThat(chat.messages()).containsExactly("hello, world!");
    // Confirm that your app made the HTTP requests you were expecting.
    RecordedRequest request1 = server.takeRequest();
    assertEquals("/v1/chat/messages/", request1.getPath());
    assertThat(request1.getHeader("Authorization")).isNotNull();
    // Shut down the server.
    server.shutdown();
    }

    View Slide

  52. WireMock
    http://wiremock.org

    View Slide

  53. Monkey
    http://developer.android.com/tools/help/monkey.html

    View Slide

  54. adb shell monkey -p your.package.name -v 500

    View Slide

  55. Caliper
    https://code.google.com/p/caliper/
    https://code.google.com/p/caliper/wiki/CaliperOnAndroid

    View Slide

  56. public class MyBenchmark extends Benchmark {
    public void timeMyOperation(int reps) {
    for (int i = 0; i < reps; i++) {
    MyClass.myOperation();
    }
    }
    }

    View Slide

  57. Vogar
    https://code.google.com/p/vogar/
    https://android.googlesource.com/platform/external/vogar/

    View Slide

  58. Vogar JAR :
    https://www.dropbox.com/s/ddjv2slngzon78r/vogar.jar?dl=0
    Sample Script and Benchmarks:
    https://github.com/segmentio/cartographer/blob/master/
    run_vogar.sh

    View Slide

  59. java -jar vogar.jar
    --benchmark
    --verbose
    --mode device
    --sourcepath …

    View Slide

  60. Questions?
    @f2prateek

    View Slide