Slide 1

Slide 1 text

Testing approach in Android Shintaro Katafuchi

Slide 2

Slide 2 text

I am… • Shintaro Katafuchi • GitHub: hotchemi • Recruit Technologies Co.,Ltd. • Scrum Engineering Group • kyobashi.dex

Slide 3

Slide 3 text

Do you write automated tests?

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

That’s it.

Slide 6

Slide 6 text

Do you know Android testing framework?

Slide 7

Slide 7 text

TestCase AndroidTestCase ActivityTestCase ActivityInstrumentationTestCase ActivityInstrumentationTestCase2 ProviderTestCase ServiceTestCase ProviderTestCase2 LoaderTestCase ApplicationTestCase ActivityUnitTestCase InstrumentationTestCase SingleLaunchActivityTestCase

Slide 8

Slide 8 text

That’s it.

Slide 9

Slide 9 text

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…

Slide 10

Slide 10 text

Why do you do testing?

Slide 11

Slide 11 text

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.

Slide 12

Slide 12 text

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.

Slide 13

Slide 13 text

What is the test target ?

Slide 14

Slide 14 text

AndroidJUnitRunner Small Medium Large has 3 annotations • @SmalTest • @MediumTest • @LargeTest

Slide 15

Slide 15 text

@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).

Slide 16

Slide 16 text

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…

Slide 17

Slide 17 text

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;
        }   }

Slide 18

Slide 18 text

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;
        }
 
 }

Slide 19

Slide 19 text

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));
        }   }

Slide 20

Slide 20 text

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.

Slide 21

Slide 21 text

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;
        }   }

Slide 22

Slide 22 text

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");
 }   }

Slide 23

Slide 23 text

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();
        }
 }

Slide 24

Slide 24 text

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);  }
        }
 }

Slide 25

Slide 25 text

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.

Slide 26

Slide 26 text

@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.

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

@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

Slide 30

Slide 30 text

public  class  HintMatcher  {
 
        static  Matcher  withHint(final  String  substring)  {
                return  withHint(is(substring));
        }
 
        static  Matcher  withHint(final  Matcher  stringMatcher)  {
                checkNotNull(stringMatcher);
                return  new  BoundedMatcher(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

Slide 31

Slide 31 text

@Rule
 public  IntentsTestRule  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

Slide 32

Slide 32 text

@Rule
        public  ActivityTestRule  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

Slide 33

Slide 33 text

@Rule
        public  ActivityTestRule  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

Slide 34

Slide 34 text

@Rule
        public  ActivityTestRule  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

Slide 35

Slide 35 text

Spoon

Slide 36

Slide 36 text

@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

Slide 37

Slide 37 text

u2020

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

@LargeTest • It’s E2E test. • Test target is that “scenario is done”. • Use real server. • It’s also regression test. • Execution time: < 120s.

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Road to automation Small

Slide 43

Slide 43 text

Road to automation Small Large

Slide 44

Slide 44 text

Road to automation Small Medium Large

Slide 45

Slide 45 text

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.

Slide 46

Slide 46 text

Thanks, Any questions?