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 full-size slide

  2. Unit Tests
    small scoped and isolated

    View full-size slide

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

    View full-size slide

  4. JUnit
    http://junit.org

    View full-size 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 full-size slide

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

    View full-size slide

  7. java.lang.RuntimeException: Stub!

    View full-size 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 full-size slide

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

    View full-size 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 full-size slide

  11. Can’t Test Outside Device

    View full-size slide

  12. Can’t Test Outside Device

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  15. Robolectric
    http://robolectric.org/

    View full-size slide

  16. @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 full-size slide

  17. @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 full-size slide

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

    View full-size slide

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

    View full-size slide

  20. 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 full-size slide

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

    View full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. Expected visibility but was .

    View full-size slide

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

    View full-size slide

  30. 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 full-size slide

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

    View full-size slide

  32. 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 full-size slide

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

    View full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  40. 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 full-size slide

  41. @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 full-size slide

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

    View full-size slide

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

    View full-size slide

  44. @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 full-size slide

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

    View full-size slide

  46. 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 full-size slide

  47. WireMock
    http://wiremock.org

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  53. 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 full-size slide

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

    View full-size slide

  55. Questions?
    @f2prateek

    View full-size slide