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

Testing approach in android

@hotchemi
August 08, 2015

Testing approach in android

For android all stars in shibuya.

@hotchemi

August 08, 2015
Tweet

More Decks by @hotchemi

Other Decks in Programming

Transcript

  1. I am… • Shintaro Katafuchi • GitHub: hotchemi • Recruit

    Technologies Co.,Ltd. • Scrum Engineering Group • kyobashi.dex
  2. Testing is “tough” part Most developers don’t know… • Why

    you do testing • What is the test target • Which frameworks you should use Actually there is no standard yet…
  3. To keep the quality Alright, what is the “quality” ?

    I think there are mainly two stuff. • App quality (external) • Code quality (internal) Which is important? It depends.
  4. We are scrum • From waterfall • 8 developers (app

    + api) and 2 testers • Release the app about twice a week • Legacy code and no automated tests! We need tests to speed up the development.
  5. @SmallTest • It’s unit test. • Test target is just

    “business logic”. • Utils, Entity, UseCase, Model… • Use mocks for external dependencies. • Execution time: < 100ms • Running on JVM (Ideally).
  6. Using… • Must: Robolectric 3, JUnit 4 • Running fast

    on the JVM • Easy to integrate with grade and Android Studio • Provides external data mock(http, preference, db) • Binds shadow objects to Android classes(JavaAssist) • Option: Mocito, PowerMock, AssertJ etc…
  7. package  toybox.robolectric;
 
 import  android.content.Intent;
 import  android.net.Uri;
 import  android.os.Bundle;
 import

     android.support.v7.app.ActionBarActivity;
 import  android.text.TextUtils;
 
 public  final  class  IntentHandler  {
        /**
          *  Handle  external  intent  and  return  destination  class.
          *
          *  @param  intent  external  intent.
          *  @return  activity  class  that  should  launch
          */
        public  static  Class<?  extends  AppCompatActivity>  handleIntent(Intent  intent)  {
                String  action  =  intent.getAction();
                Uri  uri  =  intent.getData();
                if  (!TextUtils.equals(Intent.ACTION_VIEW,  action)  ||  uri  ==  null)  {
                        return  LoginActivity.class;
                }
                if  (TextUtils.equals(uri.getScheme(),  "https")  &&  TextUtils.equals(uri.getHost(),  "twitter.com"))
                        return  TwitterActivity.class;
                }
                return  LoginActivity.class;
        }   }
  8. package  toybox.robolectric;
 
 import  org.junit.After;
 import  org.junit.Before;
 import  org.junit.runner.RunWith;
 import

     org.robolectric.RobolectricGradleTestRunner;
 import  org.robolectric.RuntimeEnvironment;
 import  org.robolectric.annotation.Config;
 
 import  android.content.Context;
 
 @Config(constants  =  BuildConfig.class,  sdk  =  19)
 @RunWith(RobolectricGradleTestRunner.class)
 public  abstract  class  AbstractRobolectricTestCase  {
 
        protected  Context  context;
 
        @Before
        public  void  setUp()  {
                context  =  RuntimeEnvironment.application.getApplicationContext();
        }
 
        @After
        public  void  tearDown()  {
                context  =  null;
        }
 
 }
  9. package  toybox.robolectric;
 
 import  android.content.Intent;
 import  android.net.Uri;
 
 import  static

     org.assertj.core.api.Assertions.assertThat;
 
 import  static  org.robolectric.Shadows.shadowOf;
 import  static  toybox.robolectric.IntentHandler.handleIntent;
 import  static  toybox.robolectric.IntentHandler.isTwitterUri;
 
 import  org.junit.Test;
 import  org.robolectric.Robolectric;
 
 public  class  IntentHandlerTest  extends  AbstractRobolectricTestCase  {
        @Test
        public  void  wrong_approach()  {
                SplashActivity  activity  =  Robolectric.setupActivity(SplashActivity.class);
                Intent  expectedIntent  =  new  Intent(activity,  LoginActivity.class);
                assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent);
        }
 }   public  class  SplashActivity  extends  AppCompatActivity  {
        @Override
        protected  void  onCreate(Bundle  savedInstanceState)  {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                Class<?>  destination  =  handleIntent(getIntent());
                startActivity(new  Intent(this,  destination));
        }   }
  10. package  toybox.robolectric;
 
 import  android.content.Intent;
 import  android.net.Uri;
 
 import  static

     org.assertj.core.api.Assertions.assertThat;
 
 import  static  org.robolectric.Shadows.shadowOf;
 import  static  toybox.robolectric.IntentHandler.handleIntent;
 import  static  toybox.robolectric.IntentHandler.isTwitterUri;
 
 import  org.junit.Test;
 import  org.robolectric.Robolectric;
 
 public  class  IntentHandlerTest  extends  AbstractRobolectricTestCase  {
        @Test
        public  void  wrong_approach()  {
                SplashActivity  activity  =  Robolectric.setupActivity(SplashActivity.class);
                Intent  expectedIntent  =  new  Intent(activity,  LoginActivity.class);
                assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent);
        }
 }   public  class  SplashActivity  extends  AppCompatActivity  {
        @Override
        protected  void  onCreate(Bundle  savedInstanceState)  {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                Class<?>  destination  =  handleIntent(getIntent());
                startActivity(new  Intent(this,  destination));
        }   } Wrong approach • It’s not unit test. • You are not shadow class master. • Just test business logic itself.
  11. package  toybox.robolectric;
 
 import  android.content.Intent;
 import  android.net.Uri;
 import  android.os.Bundle;
 import

     android.support.v7.app.ActionBarActivity;
 import  android.text.TextUtils;
 
 public  final  class  IntentHandler  {
        /**
          *  Handle  external  intent  and  return  destination  class.
          *
          *  @param  intent  external  intent.
          *  @return  activity  class  that  should  launch
          */
        public  static  Class<?  extends  ActionBarActivity>  handleIntent(Intent  intent)  {
                String  action  =  intent.getAction();
                Uri  uri  =  intent.getData();
                if  (!TextUtils.equals(Intent.ACTION_VIEW,  action)  ||  uri  ==  null)  {
                        return  LoginActivity.class;
                }
                if  (TextUtils.equals(uri.getScheme(),  "https")  &&  TextUtils.equals(uri.getHost(),  "twitter.com"))
                        return  TwitterActivity.class;
                }
                return  LoginActivity.class;
        }   }
  12. package  toybox.robolectric;
 
 import  android.content.Intent;
 import  android.net.Uri;
 import  android.os.Bundle;
 import

     android.support.v7.app.ActionBarActivity;
 import  android.text.TextUtils;
 
 public  final  class  IntentHandler  {
        /**
          *  Handle  external  intent  and  return  destination  class.
          *
          *  @param  intent  external  intent.
          *  @return  activity  class  that  should  launch
          */
        public  static  Class<?  extends  ActionBarActivity>  handleIntent(Intent  intent)  {
                String  action  =  intent.getAction();
                Uri  uri  =  intent.getData();
                if  (!TextUtils.equals(Intent.ACTION_VIEW,  action)  ||  uri  ==  null)  {
                        return  LoginActivity.class;
                }
                if  (isTwitterUri(uri.getScheme(),  uri.getHost()))  {
                        return  TwitterActivity.class;
                }
                return  LoginActivity.class;
        }   static  boolean  isTwitterUri(String  scheme,  String  host)  {
        return  TextUtils.equals(scheme,  "https")  &&  TextUtils.equals(host,  "twitter.com");
 }   }
  13. package  toybox.robolectric;
 
 import  android.content.Intent;
 import  android.net.Uri;
 
 import  static

     org.assertj.core.api.Assertions.assertThat;
 
 import  static  org.robolectric.Shadows.shadowOf;
 import  static  toybox.robolectric.IntentHandler.handleIntent;
 import  static  toybox.robolectric.IntentHandler.isTwitterUri;
 
 import  org.junit.Test;
 import  org.robolectric.Robolectric;
 
 public  class  IntentHandlerTest  extends  AbstractRobolectricTestCase  {
        @Test
        public  void  validCase_IsTwitterUri()  {
                assertThat(isTwitterUri("https",  "twitter.com")).isTrue();
        }
        @Test
        public  void  invalidCase_IsTwitterUri()  {
                assertThat(isTwitterUri("http",  "twitter.com")).isFalse();
                assertThat(isTwitterUri("https",  "facebook.com")).isFalse();
                assertThat(isTwitterUri(null,  null)).isFalse();
        }
 }
  14. package  toybox.robolectric;
 
 import  android.content.Intent;
 import  android.net.Uri;
 
 import  static

     org.assertj.core.api.Assertions.assertThat;
 
 import  static  org.robolectric.Shadows.shadowOf;
 import  static  toybox.robolectric.IntentHandler.handleIntent;
 import  static  toybox.robolectric.IntentHandler.isTwitterUri;
 
 import  org.junit.Test;
 import  org.robolectric.Robolectric;
 
 public  class  IntentHandlerTest  extends  AbstractRobolectricTestCase  {
        @Test
        public  void  validCase_HandleIntent()  {
                Intent  intent  =  new  Intent();
                intent.setAction(Intent.ACTION_VIEW);
                intent.setData(Uri.parse("https://twitter.com"));
                assertThat(handleIntent(intent)).isEqualTo(TwitterActivity.class);
        }
        @Test
        public  void  invalidCase_HandleIntent()  {
                {  Intent  intent  =  new  Intent();
                        assertThat(handleIntent(intent)).isEqualTo(LoginActivity.class);  }
                {  Intent  intent  =  new  Intent();
                        intent.setAction(Intent.ACTION_SEND);
                        assertThat(handleIntent(intent)).isEqualTo(LoginActivity.class);  }
                {  Intent  intent  =  new  Intent();
                        intent.setAction(Intent.ACTION_VIEW);
                        assertThat(handleIntent(intent)).isEqualTo(LoginActivity.class);  }
        }
 }
  15. package  toybox.robolectric;
 
 import  android.content.Intent;
 import  android.net.Uri;
 
 import  static

     org.assertj.core.api.Assertions.assertThat;
 
 import  static  org.robolectric.Shadows.shadowOf;
 import  static  toybox.robolectric.IntentHandler.handleIntent;
 import  static  toybox.robolectric.IntentHandler.isTwitterUri;
 
 import  org.junit.Test;
 import  org.robolectric.Robolectric;
 
 public  class  IntentHandlerTest  extends  AbstractRobolectricTestCase  {
        @Test
        public  void  validCase_HandleIntent()  {
                Intent  intent  =  new  Intent();
                intent.setAction(Intent.ACTION_VIEW);
                intent.setData(Uri.parse("https://twitter.com"));
                assertThat(handleIntent(intent)).isEqualTo(TwitterActivity.class);
        }
        @Test
        public  void  invalidCase_HandleIntent()  {
                {  Intent  intent  =  new  Intent();
                        assertThat(handleIntent(intent)).isEqualTo(LoginActivity.class);  }
                {  Intent  intent  =  new  Intent();
                        intent.setAction(Intent.ACTION_SEND);
                        assertThat(handleIntent(intent)).isEqualTo(LoginActivity.class);  }
                {  Intent  intent  =  new  Intent();
                        intent.setAction(Intent.ACTION_VIEW);
                        assertThat(handleIntent(intent)).isEqualTo(LoginActivity.class);  }
        }
 } Good approach • Export pure java part if you can. • Just pull in arguments and assert return stuff. • AssertJ helps you writing readable test code.
  16. @MediumTest • It’s integration test. • Test target is ui

    logic and view state. • Activity, Custom View… • Use mock dependency. • Execution time: < 2s. • Where to test? It depends.
  17. Using… • Must: Espresso 2, JUnit 4, DI framework, AndroidTestCase

    • Espresso has some components • core, contrib, intents, idling, web • Using DI and makes app testable (Dagger 2 is awesome!!) • Option: assertj-android, Spoon, Mockito • Use AndroidTestCase for simple custom view • Spoon manages multiple devices screenshot
  18. public  class  PlannedAndCompleteLessonViewTest  extends  AndroidTestCase  {
 
    private  PlannedAndCompleteLessonView

     customView;
 
    private  TextView  plannedCountTextView;
 
    private  TextView  completeCountTextView;
 
    @Override
    protected  void  setUp()  throws  Exception  {
        super.setUp();
        customView  =  new  PlannedAndCompleteLessonView(getContext());
        plannedCountTextView  =  (TextView)  customView
                        .findViewById(R.id.view_planned_and_complete_lesson_planned_count);
        completeCountTextView  =  (TextView)  customView
                        .findViewById(R.id.view_planned_and_complete_lesson_complete_count);
    }
 
    public  void  testInitialState()  {
        assertEquals(getColor(R.color.very_light_grey),  plannedCountTextView.getCurrentTextColor());
        assertEquals(getColor(R.color.navy_blue),  completeCountTextView.getCurrentTextColor());
        assertEquals("",  plannedCountTextView.getText());
        assertEquals(getContext().getString(R.string.label_home_planned_lesson_hyphen),  plannedCountTextView.getHint());
        assertEquals("0",  completeCountTextView.getText());
    }   } Custom View
  19. @Test
 public  void  changeText_sameActivity()  {
        //  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)));
 }
 
 @Test
 public  void  changeText_newActivity()  {
        //  Type  text  and  then  press  the  button.
        onView(withId(R.id.editTextUserInput)).perform(typeText(STRING_TO_BE_TYPED),
                        closeSoftKeyboard());
        onView(withId(R.id.activityChangeTextBtn)).perform(click());
 
        //  This  view  is  in  a  different  Activity,  no  need  to  tell  Espresso.
        onView(withId(R.id.show_text_view)).check(matches(withText(STRING_TO_BE_TYPED)));
 }   Espresso-core
  20. public  class  HintMatcher  {
 
        static  Matcher<View>

     withHint(final  String  substring)  {
                return  withHint(is(substring));
        }
 
        static  Matcher<View>  withHint(final  Matcher<String>  stringMatcher)  {
                checkNotNull(stringMatcher);
                return  new  BoundedMatcher<View,  EditText>(EditText.class)  {
 
                        @Override
                        public  boolean  matchesSafely(EditText  view)  {
                                final  CharSequence  hint  =  view.getHint();
                                return  hint  !=  null  &&  stringMatcher.matches(hint.toString());
                        }
 
                        @Override
                        public  void  describeTo(Description  description)  {
                                description.appendText("with  hint:  ");
                                stringMatcher.describeTo(description);
                        }
                };
        }
 }   Custom Matcher
  21. @Rule
 public  IntentsTestRule<DialerActivity>  mActivityRule  =  new  IntentsTestRule<>(
      

             DialerActivity.class);
 
 @Before
 public  void  stubAllExternalIntents()  {
        //  By  default  Espresso  Intents  does  not  stub  any  Intents.  Stubbing  needs  to  be  setup  before
        //  every  test  run.  In  this  case  all  external  Intents  will  be  blocked.
        intending(not(isInternal())).respondWith(new  ActivityResult(Activity.RESULT_OK,  null));
 }
 
 @Test
 public  void  typeNumber_ValidInput_InitiatesCall()  {
        //  Types  a  phone  number  into  the  dialer  edit  text  field  and  presses  the  call  button.
        onView(withId(R.id.edit_text_caller_number))
                        .perform(typeText(VALID_PHONE_NUMBER),  closeSoftKeyboard());
        onView(withId(R.id.button_call_number)).perform(click());
 
        //  Verify  that  an  intent  to  the  dialer  was  sent  with  the  correct  action,  phone
        //  number  and  package.  Think  of  Intents  intended  API  as  the  equivalent  to  Mockito's  verify.
        intended(allOf(
                        hasAction(Intent.ACTION_CALL),
                        hasData(INTENT_DATA_PHONE_NUMBER),
                        toPackage(PACKAGE_ANDROID_DIALER)));
 }   Espresso-intents
  22. @Rule
        public  ActivityTestRule<ShowWebViewActivity>  activityRule  =  new  ActivityTestRule<>(


                           ShowWebViewActivity.class);
 
        @Test
        public  void  initial_state()  {
                onWebView().check(webContent(hasElementWithId("email_input")));
                onWebView().check(webContent(hasElementWithId("password_input")));
                onWebView().check(webContent(hasElementWithId("submit_button")));
        }
 
        @Test
        public  void  enter_email_and_password_submit()  {
                onWebView().withElement(findElement(Locator.ID,  "email_input"))
                                .perform(webKeys(EMAIL_TO_BE_TYPED));
                onWebView().withElement(findElement(Locator.ID,  "password_input"))
                                .perform(webKeys(PASSWORD_TO_BE_TYPED));
                onWebView().withElement(findElement(Locator.ID,  "submit_button"))
                                .perform(webClick());
 
                //  This  view  is  in  a  different  Activity,  no  need  to  tell  Espresso.
                onView(withId(R.id.email)).check(matches(withText(EMAIL_TO_BE_TYPED)));
                onView(withId(R.id.password)).check(matches(withText(PASSWORD_TO_BE_TYPED)));
        }   Espresso-web
  23. @Rule
        public  ActivityTestRule<ShowWebViewActivity>  activityRule  =  new  ActivityTestRule<>(


                           ShowWebViewActivity.class);
 
        @Test
        public  void  initial_state()  {
                onWebView().check(webContent(hasElementWithId("email_input")));
                onWebView().check(webContent(hasElementWithId("password_input")));
                onWebView().check(webContent(hasElementWithId("submit_button")));
        }
 
        @Test
        public  void  enter_email_and_password_submit()  {
                signInWithIdAndPassword(EMAIL_TO_BE_TYPED,  PASSWORD_TO_BE_TYPED);
                //  This  view  is  in  a  different  Activity,  no  need  to  tell  Espresso.
                onView(withId(R.id.email)).check(matches(withText(EMAIL_TO_BE_TYPED)));
                onView(withId(R.id.password)).check(matches(withText(PASSWORD_TO_BE_TYPED)));
        }   private  static  void  signInWithIdAndPassword(String  id,  String  password)  {
        onWebView().withElement(findElement(Locator.ID,  "email_input"))
                        .perform(webKeys(id));
        onWebView().withElement(findElement(Locator.ID,  "password_input"))
                        .perform(webKeys(password));
        onWebView().withElement(findElement(Locator.ID,  "submit_button"))
                        .perform(webClick());
 }   Espresso-web
  24. @Rule
        public  ActivityTestRule<ShowWebViewActivity>  activityRule  =  new  ActivityTestRule<>(


                           ShowWebViewActivity.class);
 
        @Test
        public  void  initial_state()  {
                onWebView().check(webContent(hasElementWithId("email_input")));
                onWebView().check(webContent(hasElementWithId("password_input")));
                onWebView().check(webContent(hasElementWithId("submit_button")));
        }
 
        @Test
        public  void  enter_email_and_password_submit()  {
                signInWithIdAndPassword(EMAIL_TO_BE_TYPED,  PASSWORD_TO_BE_TYPED);
                //  This  view  is  in  a  different  Activity,  no  need  to  tell  Espresso.
                onView(withId(R.id.email)).check(matches(withText(EMAIL_TO_BE_TYPED)));
                onView(withId(R.id.password)).check(matches(withText(PASSWORD_TO_BE_TYPED)));
        }   private  static  void  signInWithIdAndPassword(String  id,  String  password)  {
        onWebView().withElement(findElement(Locator.ID,  "email_input"))
                        .perform(webKeys(id));
        onWebView().withElement(findElement(Locator.ID,  "password_input"))
                        .perform(webKeys(password));
        onWebView().withElement(findElement(Locator.ID,  "submit_button"))
                        .perform(webClick());
 }   Espresso-web Good approach • Don’t write onView each test case • Create your own DSL
  25. @Module
 public  class  MockClockModule  {
    @Provides
    @Singleton
  

     Clock  provideClock()  {
        return  Mockito.mock(Clock.class);
    }
 }   @Inject
 Clock  clock;
 
 @Singleton
 @Component(modules  =  MockClockModule.class)
 public  interface  TestComponent  extends  DemoComponent  {
    void  inject(MainActivityTest  mainActivityTest);
 }   
 @Before
 public  void  setUp()  {
    Instrumentation  instrumentation  =  InstrumentationRegistry.getInstrumentation();
    DemoApplication  app
            =  (DemoApplication)  instrumentation.getTargetContext().getApplicationContext();
    TestComponent  component  =  DaggerMainActivityTest_TestComponent.builder()
            .mockClockModule(new  MockClockModule())
            .build();
    app.setComponent(component);
    component.inject(this);
 }   
 @Test
 public  void  today()  {
    Mockito.when(clock.getNow()).thenReturn(new  DateTime(2008,  9,  23,  0,  0,  0));
    activityRule.launchActivity(new  Intent());
    onView(withId(R.id.date))
            .check(matches(withText("2008-­‐09-­‐23")));
 } DI
  26. package  com.jakewharton.u2020.data;
 
 import  android.content.Intent;
 import  android.net.Uri;
 
 /**  Creates

     {@link  Intent}s  for  launching  into  external  applications.  */
 public  interface  IntentFactory  {
        Intent  createUrlIntent(String  url);
 
        IntentFactory  REAL  =  new  IntentFactory()  {
                @Override  public  Intent  createUrlIntent(String  url)  {
                        Intent  intent  =  new  Intent(Intent.ACTION_VIEW);
                        intent.setData(Uri.parse(url));
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
                        return  intent;
                }
        };
 }   u2020
  27. package  com.jakewharton.u2020.data;
 
 import  android.content.Intent;
 import  com.jakewharton.u2020.data.prefs.BooleanPreference;
 import  com.jakewharton.u2020.ui.ExternalIntentActivity;
 


    /**
  *  An  {@link  IntentFactory}  implementation  that  wraps  all  {@code  Intent}s  with  a  debug  action,  which
  *  launches  an  activity  that  allows  you  to  inspect  the  content.
  */
 public  final  class  DebugIntentFactory  implements  IntentFactory  {
        private  final  IntentFactory  realIntentFactory;
        private  final  boolean  isMockMode;
        private  final  BooleanPreference  captureIntents;
 
        public  DebugIntentFactory(IntentFactory  realIntentFactory,  boolean  isMockMode,
                                                            BooleanPreference  captureIntents)  {
                this.realIntentFactory  =  realIntentFactory;
                this.isMockMode  =  isMockMode;
                this.captureIntents  =  captureIntents;
        }
 
        @Override  public  Intent  createUrlIntent(String  url)  {
                Intent  baseIntent  =  realIntentFactory.createUrlIntent(url);
                if  (!isMockMode  ||  !captureIntents.get())  {
                        return  baseIntent;
                }  else  {
                        return  ExternalIntentActivity.createIntent(baseIntent);
                }
        }
 } u2020
  28. @LargeTest • It’s E2E test. • Test target is that

    “scenario is done”. • Use real server. • It’s also regression test. • Execution time: < 120s.
  29. Using… • Umm, it depends. • If you have test

    engineer and iOS app… • Appium • If you have only qa testers… • Calabash-android • If you focus on modern android… • UI automator • Other than that… • robotium-recorder
  30. Conclusion • Don’t be a code monkey. • Plan and

    do retrospective. • Automation is not the silver bullet • We can’t test animation, usability • Do I have to write full set of tests? • It depends on your service level.