Automated testing with Espresso2.x

Preview:

Citation preview

Espresso 2.xAutomated testing with

Tatsuya MAKI Android Application Developer

Ⅰなぜやるのか

Ⅱ Ⅲどこに使うか 基本のおさらい

Ⅳ5つのポイント

Ⅰなぜやるのか

要件定義 設計 リリース開発 テスト

要件定義 設計 リリース開発 テスト

プロセスを効率化すれば改善サイクルが速くなる。

多くのプロセスは自動化しにくい。

要件定義 設計 リリース開発 テスト

テストやリリースは自動化しやすい。

要件定義 設計 リリース開発 テスト

開発サイクルを効率的に回すためにテストの自動化が重要。

Ⅱどこに使うか

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

多くのアプリケーションは多層アーキテクチャで構成される。

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

結合テストが必要なレイヤはEspressoが適する。

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

単体テストで十分なレイヤはRobolectricが適する。

レイヤに応じた適切なフレームワークを選択する。

Ⅲ基本のおさらい

@RunWith(AndroidJUnit4.class)public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, true, false); @Test public void shouldLaunchActivity() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); final Activity activity = mActivityRule.launchActivity(intent); onView(withId(R.id.navigation_icon)).perform(click()); onView(withId(R.id.my_view)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); }}

@RunWith(AndroidJUnit4.class)public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, true, false); @Test public void shouldLaunchActivity() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); final Activity activity = mActivityRule.launchActivity(intent); onView(withId(R.id.navigation_icon)).perform(click()); onView(withId(R.id.my_view)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); }}

テストランナーにAndroidJUnit4を指定。

@RunWith(AndroidJUnit4.class)public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, true, false); @Test public void shouldLaunchActivity() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); final Activity activity = mActivityRule.launchActivity(intent); onView(withId(R.id.navigation_icon)).perform(click()); onView(withId(R.id.my_view)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); }}

ActivityTestRuleでテスト対象を指定。

@RunWith(AndroidJUnit4.class)public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, true, false); @Test public void shouldLaunchActivity() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); final Activity activity = mActivityRule.launchActivity(intent); onView(withId(R.id.navigation_icon)).perform(click()); onView(withId(R.id.my_view)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); }}

Activityの起動はActivityTestRule経由。

@RunWith(AndroidJUnit4.class)public class MainActivityTest { @Rule public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, true, false); @Test public void shouldLaunchActivity() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); final Activity activity = mActivityRule.launchActivity(intent); onView(withId(R.id.navigation_icon)).perform(click()); onView(withId(R.id.my_view)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); }}

UI取得やUI操作はEspresso経由。

Espressoでテストを書くハードルはそこまで高くない。

Ⅳ5つのポイント

ページオブジェクトを導入する。

外部依存を可能な限り排除する。

インスタンスを変更可能にする。1

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

ページオブジェクトを導入する。

外部依存を可能な限り排除する。

インスタンスを変更可能にする。1

2

3

4

5 EspressoのAPIをラップする。

非同期処理の待機にSleepを避ける。

public class VolleyProvider { private static RequestQueue sRequestQueue; private static ImageLoader sImageLoader; public static RequestQueue getRequestQueue(Context context) { if (sRequestQueue == null) { sRequestQueue = Volley.newRequestQueue(context); } return sRequestQueue; } public static ImageLoader getImageLoader(Context context) { if (sImageLoader == null) { final RequestQueue queue = getRequestQueue(context); sImageLoader = new ImageLoader(queue, new LruBitmapCache()); } return sImageLoader; }}

public class VolleyProvider { private static RequestQueue sRequestQueue; private static ImageLoader sImageLoader; public static RequestQueue getRequestQueue(Context context) { if (sRequestQueue == null) { sRequestQueue = Volley.newRequestQueue(context); } return sRequestQueue; } public static ImageLoader getImageLoader(Context context) { if (sImageLoader == null) { final RequestQueue queue = getRequestQueue(context); sImageLoader = new ImageLoader(queue, new LruBitmapCache()); } return sImageLoader; }}

RequestQueueのインスタンスが変更不可能。

public class VolleyProvider { private static RequestQueue sRequestQueue; private static ImageLoader sImageLoader; public static RequestQueue getRequestQueue(Context context) { if (sRequestQueue == null) { sRequestQueue = Volley.newRequestQueue(context); } return sRequestQueue; } public static ImageLoader getImageLoader(Context context) { if (sImageLoader == null) { final RequestQueue queue = getRequestQueue(context); sImageLoader = new ImageLoader(queue, new LruBitmapCache()); } return sImageLoader; }}

ImageLoaderのインスタンスも変更不可能。

依存するインスタンスの振舞に応じたテストができない。

依存性注入の仕組みを導入する。

public class VolleyProvider { private static RequestQueue sRequestQueue; private static ImageLoader sImageLoader; public static RequestQueue getRequestQueue(Context context) { if (sRequestQueue == null) { sRequestQueue = Volley.newRequestQueue(context); } return sRequestQueue; } public static ImageLoader getImageLoader(Context context) { if (sImageLoader == null) { final RequestQueue queue = getRequestQueue(context); sImageLoader = new ImageLoader(queue, new BitmapCache()); } return sImageLoader; } static void setRequestQueue(RequestQueue queue) { sRequestQueue = queue; } static void setImageLoader(ImageLoader loader) { sImageLoader = loader; }}

public class VolleyProvider { private static RequestQueue sRequestQueue; private static ImageLoader sImageLoader; public static RequestQueue getRequestQueue(Context context) { if (sRequestQueue == null) { sRequestQueue = Volley.newRequestQueue(context); } return sRequestQueue; } public static ImageLoader getImageLoader(Context context) { if (sImageLoader == null) { final RequestQueue queue = getRequestQueue(context); sImageLoader = new ImageLoader(queue, new BitmapCache()); } return sImageLoader; } static void setRequestQueue(RequestQueue queue) { sRequestQueue = queue; } static void setImageLoader(ImageLoader loader) { sImageLoader = loader; }}

RequestQueueのインスタンスが変更可能。

public class VolleyProvider { private static RequestQueue sRequestQueue; private static ImageLoader sImageLoader; public static RequestQueue getRequestQueue(Context context) { if (sRequestQueue == null) { sRequestQueue = Volley.newRequestQueue(context); } return sRequestQueue; } public static ImageLoader getImageLoader(Context context) { if (sImageLoader == null) { final RequestQueue queue = getRequestQueue(context); sImageLoader = new ImageLoader(queue, new BitmapCache()); } return sImageLoader; } static void setRequestQueue(RequestQueue queue) { sRequestQueue = queue; } static void setImageLoader(ImageLoader loader) { sImageLoader = loader; }}

ImageLoaderのインスタンスが変更可能。

インスタンスを変更可能な実装にしてメンテナビリティを高める。

ページオブジェクトを導入する。

外部依存を可能な限り排除する。

インスタンスを変更可能にする。1

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

@Testpublic void shouldShowLoadingViewWhenRequestIsLoading() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowContentWhenRequestSucceeded() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)));}

@Testpublic void shouldShowLoadingViewWhenRequestIsLoading() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowFailureViewWhenRequestIsFailed() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)));}

読込中の表示を検証したい。

@Testpublic void shouldShowLoadingViewWhenRequestIsLoading() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowFailureViewWhenRequestIsFailed() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)));}

読込成功の表示を検証したい。

@Testpublic void shouldShowLoadingViewWhenRequestIsLoading() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowFailureViewWhenRequestIsFailed() { launchActivity(); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)));}

読込失敗の表示を検証したい。

外部に依存する振舞をテストし切れない。

テスト用のインスタンスに変更して振舞を制御する。

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { final NetworkDispatcher dispatcher = mRequestQueue.getNetworkDispatcher(); dispatcher.append( new BasicRequestMatcher.Builder() .setMethodMatcher(MethodMatcher.GET) .setUrlPattern("^https://ajax.googleapis.com/ajax/services/feed/load.+") .build(), new NetworkResponseBuilder() .setStatusCode(StatusCode.OK) .addHeader("Content-Type", "application/json") .setBody(mAssetReader.read("feed_load_success_10.json")) .build() ); mRequestQueue.resume(); final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { final NetworkDispatcher dispatcher = mRequestQueue.getNetworkDispatcher(); dispatcher.append( new BasicRequestMatcher.Builder() .setMethodMatcher(MethodMatcher.GET) .setUrlPattern("^https://ajax.googleapis.com/ajax/services/feed/load.+") .build(), new NetworkResponseBuilder() .setStatusCode(StatusCode.OK) .addHeader("Content-Type", "application/json") .setBody(mAssetReader.read("feed_load_success_10.json")) .build() ); mRequestQueue.resume(); final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

外部に依存する振舞を事前に変更しておく。

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { final NetworkDispatcher dispatcher = mRequestQueue.getNetworkDispatcher(); dispatcher.append( new BasicRequestMatcher.Builder() .setMethodMatcher(MethodMatcher.GET) .setUrlPattern("^https://ajax.googleapis.com/ajax/services/feed/load.+") .build(), new NetworkResponseBuilder() .setStatusCode(StatusCode.OK) .addHeader("Content-Type", "application/json") .setBody(mAssetReader.read("feed_load_success_10.json")) .build() ); mRequestQueue.resume(); final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

マッチさせるリクエストのパターンを指定。

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { final NetworkDispatcher dispatcher = mRequestQueue.getNetworkDispatcher(); dispatcher.append( new BasicRequestMatcher.Builder() .setMethodMatcher(MethodMatcher.GET) .setUrlPattern("^https://ajax.googleapis.com/ajax/services/feed/load.+") .build(), new NetworkResponseBuilder() .setStatusCode(StatusCode.OK) .addHeader("Content-Type", "application/json") .setBody(mAssetReader.read("feed_load_success_10.json")) .build() ); mRequestQueue.resume(); final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

パターンにマッチした際のレスポンスを指定。

@Testpublic void shouldShowSuccessViewWhenRequestIsSucceeded() { final NetworkDispatcher dispatcher = mRequestQueue.getNetworkDispatcher(); dispatcher.append( new BasicRequestMatcher.Builder() .setMethodMatcher(MethodMatcher.GET) .setUrlPattern("^https://ajax.googleapis.com/ajax/services/feed/load.+") .build(), new NetworkResponseBuilder() .setStatusCode(StatusCode.OK) .addHeader("Content-Type", "application/json") .setBody(mAssetReader.read("feed_load_success_10.json")) .build() ); mRequestQueue.resume(); final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

事前処理が終わったらテストを実行する。

外部依存を排除できる実装にしてテスタビリティを高める。

ページオブジェクトを導入する。

外部依存を可能な限り排除する。

インスタンスを変更可能にする。1

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

@Testpublic void shouldShowContentWhenRequestSucceeded() throws InterruptedException { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); Thread.sleep(5000); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowContentWhenRequestSucceeded() throws InterruptedException { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); Thread.sleep(5000); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

該当のActivityを起動する。

@Testpublic void shouldShowContentWhenRequestSucceeded() throws InterruptedException { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); Thread.sleep(5000); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

非同期処理が完了するまで待機する。

@Testpublic void shouldShowContentWhenRequestSucceeded() throws InterruptedException { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); Thread.sleep(5000); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

非同期処理完了時の表示を検証する。

Sleepを含むテストは壊れやすくスローテストに陥りやすい。

非同期処理の完了待ちにIdlingResourceが使えるようにする。

public class TestRequestQueue extends RequestQueue { private List<RequestAddedListener> mAddedListeners = new ArrayList<>(); @Override public <T> Request<T> add(Request<T> request) { synchronized (mAddedListeners) { for (RequestAddedListener<T> listener : mAddedListeners) { listener.onRequestAdded(request); } } return super.add(request); } public <T> void addRequestAddedListener(RequestAddedListener<T> listener) { synchronized (mAddedListeners) { mAddedListeners.add(listener); } } public <T> void removeRequestAddedListener(RequestAddedListener<T> listener) { synchronized (mAddedListeners) { mAddedListeners.remove(listener); } } public interface RequestAddedListener<T> { void onRequestAdded(Request<T> request); }}

public class TestRequestQueue extends RequestQueue { private List<RequestAddedListener> mAddedListeners = new ArrayList<>(); @Override public <T> Request<T> add(Request<T> request) { synchronized (mAddedListeners) { for (RequestAddedListener<T> listener : mAddedListeners) { listener.onRequestAdded(request); } } return super.add(request); } public <T> void addRequestAddedListener(RequestAddedListener<T> listener) { synchronized (mAddedListeners) { mAddedListeners.add(listener); } } public <T> void removeRequestAddedListener(RequestAddedListener<T> listener) { synchronized (mAddedListeners) { mAddedListeners.remove(listener); } } public interface RequestAddedListener<T> { void onRequestAdded(Request<T> request); }}

非同期処理の開始と完了を検知可能にする。

public class RequestQueueListener implements RequestAddedListener, RequestFinishedListener { private final CountingIdlingResource mIdlingResource; public RequestQueueListener(CountingIdlingResource idlingResource) { mIdlingResource = idlingResource; } @Override public void onRequestAdded(Request request) { mIdlingResource.increment(); } @Override public void onRequestFinished(Request request) { mIdlingResource.decrement(); }}

public class RequestQueueListener implements RequestAddedListener, RequestFinishedListener { private final CountingIdlingResource mIdlingResource; public RequestQueueListener(CountingIdlingResource idlingResource) { mIdlingResource = idlingResource; } @Override public void onRequestAdded(Request request) { mIdlingResource.increment(); } @Override public void onRequestFinished(Request request) { mIdlingResource.decrement(); }}

非同期処理の開始をIdlingResourceに通知。

public class RequestQueueListener implements RequestAddedListener, RequestFinishedListener { private final CountingIdlingResource mIdlingResource; public RequestQueueListener(CountingIdlingResource idlingResource) { mIdlingResource = idlingResource; } @Override public void onRequestAdded(Request request) { mIdlingResource.increment(); } @Override public void onRequestFinished(Request request) { mIdlingResource.decrement(); }}

非同期処理の完了をIdlingResourceに通知。

@Beforepublic void setUp() { Context context = InstrumentationRegistry.getContext(); mRequestQueue = (MockRequestQueue) VolleyProvider.getRequestQueue(context); mIdlingResource = new CountingIdlingResource(RequestQueue.class.getName()); registerIdlingResources(mIdlingResource); RequestQueueListener queueListener = new RequestQueueListener(mIdlingResource); mRequestQueue.addRequestAddedListener(queueListener); mRequestQueue.addRequestFinishedListener(queueListener);}

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { ... }@Afterpublic void tearDown() { unregisterIdlingResources(mIdlingResource);}

@Beforepublic void setUp() { Context context = InstrumentationRegistry.getContext(); mRequestQueue = (MockRequestQueue) VolleyProvider.getRequestQueue(context); mIdlingResource = new CountingIdlingResource(RequestQueue.class.getName()); registerIdlingResources(mIdlingResource); RequestQueueListener queueListener = new RequestQueueListener(mIdlingResource); mRequestQueue.addRequestAddedListener(queueListener); mRequestQueue.addRequestFinishedListener(queueListener);}

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { ... }@Afterpublic void tearDown() { unregisterIdlingResources(mIdlingResource);}

事前処理でIdlingResourceを登録する。

@Beforepublic void setUp() { Context context = InstrumentationRegistry.getContext(); mRequestQueue = (MockRequestQueue) VolleyProvider.getRequestQueue(context); mIdlingResource = new CountingIdlingResource(RequestQueue.class.getName()); registerIdlingResources(mIdlingResource); RequestQueueListener queueListener = new RequestQueueListener(mIdlingResource); mRequestQueue.addRequestAddedListener(queueListener); mRequestQueue.addRequestFinishedListener(queueListener);}

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { ... }@Afterpublic void tearDown() { unregisterIdlingResources(mIdlingResource);}

事後処理でIdlingResourceを解除する。

Sleepを可能な限り避けテストのメンテナビリティを高める。

ページオブジェクトを導入する。

外部依存を可能な限り排除する。

インスタンスを変更可能にする。1

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

TestCase UI

TestCase

TestCaseonView()

onView()

onView()

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.navigation_icon)).perform(click()); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.navigation_icon)).perform(click()); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

UI操作を直接記述する。

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.navigation_icon)).perform(click()); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

UI取得を直接記述する。

UI操作やUI取得を直接記述するとUI変更に弱い。

ページオブジェクトを導入しUI操作を集約する。

TestCase PageObject

TestCase

TestCasefindLoadingView()

findLoadingView()

findLoadingView()

UIonView()

public class EntryListPage { public EntryListPage() { } public void clickNavigationIcon() { onView(withId(R.id.navigation_icon)).perform(click()); } public void pullToRefresh() { onView(withId(android.R.id.content)).perform(swipeDown()); } public ViewInteraction findLoadingView() { return onView(withId(R.id.entry_list_loading)); } public ViewInteraction findSuccessView() { return onView(withId(R.id.entry_list_success)); } public ViewInteraction findFailureView() { return onView(withId(R.id.entry_list_failure)); }}

public class EntryListPage { public EntryListPage() { } public void clickNavigationIcon() { onView(withId(R.id.navigation_icon)).perform(click()); } public void pullToRefresh() { onView(withId(android.R.id.content)).perform(swipeDown()); } public ViewInteraction findLoadingView() { return onView(withId(R.id.entry_list_loading)); } public ViewInteraction findSuccessView() { return onView(withId(R.id.entry_list_success)); } public ViewInteraction findFailureView() { return onView(withId(R.id.entry_list_failure)); }}

UI操作をページオブジェクトに集約する。

public class EntryListPage { public EntryListPage() { } public void clickNavigationIcon() { onView(withId(R.id.navigation_icon)).perform(click()); } public void pullToRefresh() { onView(withId(android.R.id.content)).perform(swipeDown()); } public ViewInteraction findLoadingView() { return onView(withId(R.id.entry_list_loading)); } public ViewInteraction findSuccessView() { return onView(withId(R.id.entry_list_success)); } public ViewInteraction findFailureView() { return onView(withId(R.id.entry_list_failure)); }}

UI取得もページオブジェクトに集約する。

@Testpublic void shouldShowContentWhenRequestSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); final EntryListPage page = new EntryListPage(); page.clickNavigationIcon(); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); }

@Testpublic void shouldShowContentWhenRequestSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); final EntryListPage page = new EntryListPage(); page.clickNavigationIcon(); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); }

UI操作はページオブジェクトを経由する。

@Testpublic void shouldShowContentWhenRequestSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = MainActivity.createIntent(context); mActivityRule.launchActivity(intent); final EntryListPage page = new EntryListPage(); page.clickNavigationIcon(); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); page.findFailureView() .check(matches(withEffectiveVisibility(Visibility.GONE))); }

UI取得もページオブジェクトを経由する。

ページオブジェクトを導入してテストのメンテナビリティを高める。

ページオブジェクトを導入する。

外部依存を可能な限り排除する。

インスタンスを変更可能にする。1

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.entry_list_refresh)).perform(click()); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.entry_list_refresh)).perform(click()); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

該当IDのViewをクリックする。

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onView(withId(R.id.entry_list_refresh)).perform(click()); onView(withId(R.id.entry_list_loading)) .check(matches(withEffectiveVisibility(Visibility.GONE))); onView(withId(R.id.entry_list_success)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))); onView(withId(R.id.entry_list_failure)) .check(matches(withEffectiveVisibility(Visibility.GONE)));}

Viewの可視性を検証する。

記述が冗長で可読性も低い。

EspressoのAPIをラップしシンプルな記述を可能にする。

public static ViewInteraction onViewById(int id) { return onView(withId(id));}public static ViewAssertion isVisible() { return hasVisibility(ViewMatchers.Visibility.VISIBLE);}public static ViewAssertion isInvisible() { return hasVisibility(ViewMatchers.Visibility.INVISIBLE);}public static ViewAssertion isGone() { return hasVisibility(ViewMatchers.Visibility.GONE);}private static ViewAssertion hasVisibility(ViewMatchers.Visibility visibility) { return matches(withEffectiveVisibility(visibility));}

public static ViewInteraction onViewById(int id) { return onView(withId(id));}public static ViewAssertion isVisible() { return hasVisibility(ViewMatchers.Visibility.VISIBLE);}public static ViewAssertion isInvisible() { return hasVisibility(ViewMatchers.Visibility.INVISIBLE);}public static ViewAssertion isGone() { return hasVisibility(ViewMatchers.Visibility.GONE);}private static ViewAssertion hasVisibility(ViewMatchers.Visibility visibility) { return matches(withEffectiveVisibility(visibility));}

IDからViewInteractionを取得する。

public static ViewInteraction onViewById(int id) { return onView(withId(id));}public static ViewAssertion isVisible() { return hasVisibility(ViewMatchers.Visibility.VISIBLE);}public static ViewAssertion isInvisible() { return hasVisibility(ViewMatchers.Visibility.INVISIBLE);}public static ViewAssertion isGone() { return hasVisibility(ViewMatchers.Visibility.GONE);}private static ViewAssertion hasVisibility(ViewMatchers.Visibility visibility) { return matches(withEffectiveVisibility(visibility));}

Viewの可視性を検証する。

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onViewById(R.id.entry_list_refresh)).perform(click()); onViewById(R.id.entry_list_loading)).check(isGone()); onViewById(R.id.entry_list_success)).check(isVisible()); onViewById(R.id.entry_list_failure)).check(isGone());}

@Testpublic void shouldShowContentWhenRequestIsSucceeded() { final Context context = InstrumentationRegistry.getTargetContext(); final Intent intent = EntryListActivity.createIntent(context); mActivityRule.launchActivity(intent); onViewById(R.id.entry_list_refresh)).perform(click()); onViewById(R.id.entry_list_loading)).check(isGone()); onViewById(R.id.entry_list_success)).check(isVisible()); onViewById(R.id.entry_list_failure)).check(isGone());}

可読性が圧倒的に向上する。

EspressoのAPIをラップして可読性を高める。

まとめ

1.開発サイクルを効率的に回すためにテストの自動化が重要。

2.レイヤに応じた適切なフレームワークを選択する。

3. Espressoでテストを書くハードルはそこまで高くない。

4. 継続しやすいテストを書く基本的なポイントを押さえる。