automated testing with espresso2.x

94
Espresso 2.x Automated testing with

Upload: tatsuya-maki

Post on 07-Aug-2015

54 views

Category:

Technology


3 download

TRANSCRIPT

Page 1: Automated testing with Espresso2.x

Espresso 2.xAutomated testing with

Page 2: Automated testing with Espresso2.x

Tatsuya MAKI Android Application Developer

Page 3: Automated testing with Espresso2.x

Ⅰなぜやるのか

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

Ⅳ5つのポイント

Page 4: Automated testing with Espresso2.x

Ⅰなぜやるのか

Page 5: Automated testing with Espresso2.x

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

Page 6: Automated testing with Espresso2.x

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

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

Page 7: Automated testing with Espresso2.x

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

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

Page 8: Automated testing with Espresso2.x

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

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

Page 9: Automated testing with Espresso2.x

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

Page 10: Automated testing with Espresso2.x

Ⅱどこに使うか

Page 11: Automated testing with Espresso2.x

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

Page 12: Automated testing with Espresso2.x

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

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

Page 13: Automated testing with Espresso2.x

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

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

Page 14: Automated testing with Espresso2.x

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

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

Page 15: Automated testing with Espresso2.x

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

Page 16: Automated testing with Espresso2.x

Ⅲ基本のおさらい

Page 17: Automated testing with Espresso2.x

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

Page 18: Automated testing with Espresso2.x

@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を指定。

Page 19: Automated testing with Espresso2.x

@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でテスト対象を指定。

Page 20: Automated testing with Espresso2.x

@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経由。

Page 21: Automated testing with Espresso2.x

@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経由。

Page 22: Automated testing with Espresso2.x

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

Page 23: Automated testing with Espresso2.x

Ⅳ5つのポイント

Page 24: Automated testing with Espresso2.x

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

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

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

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

Page 25: Automated testing with Espresso2.x

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

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

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

2

3

4

5 EspressoのAPIをラップする。

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

Page 26: Automated testing with Espresso2.x

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

Page 27: Automated testing with Espresso2.x

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のインスタンスが変更不可能。

Page 28: Automated testing with Espresso2.x

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のインスタンスも変更不可能。

Page 29: Automated testing with Espresso2.x

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

Page 30: Automated testing with Espresso2.x

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

Page 31: Automated testing with Espresso2.x

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

Page 32: Automated testing with Espresso2.x

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のインスタンスが変更可能。

Page 33: Automated testing with Espresso2.x

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のインスタンスが変更可能。

Page 34: Automated testing with Espresso2.x

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

Page 35: Automated testing with Espresso2.x

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

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

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

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

Page 36: Automated testing with Espresso2.x

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

Page 37: Automated testing with Espresso2.x

Person Presentation Layer

Domain Layer

Web API

SQLite Database

Shared Preferences

Data Layer

Page 38: Automated testing with Espresso2.x

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

Page 39: Automated testing with Espresso2.x

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

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

Page 40: Automated testing with Espresso2.x

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

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

Page 41: Automated testing with Espresso2.x

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

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

Page 42: Automated testing with Espresso2.x

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

Page 43: Automated testing with Espresso2.x

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

Page 44: Automated testing with Espresso2.x

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

Page 45: Automated testing with Espresso2.x

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

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

Page 46: Automated testing with Espresso2.x

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

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

Page 47: Automated testing with Espresso2.x

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

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

Page 48: Automated testing with Espresso2.x

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

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

Page 49: Automated testing with Espresso2.x

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

Page 50: Automated testing with Espresso2.x

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

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

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

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

Page 51: Automated testing with Espresso2.x

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

Page 52: Automated testing with Espresso2.x

@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を起動する。

Page 53: Automated testing with Espresso2.x

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

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

Page 54: Automated testing with Espresso2.x

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

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

Page 55: Automated testing with Espresso2.x

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

Page 56: Automated testing with Espresso2.x

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

Page 57: Automated testing with Espresso2.x

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

Page 58: Automated testing with Espresso2.x

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

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

Page 59: Automated testing with Espresso2.x

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

Page 60: Automated testing with Espresso2.x

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に通知。

Page 61: Automated testing with Espresso2.x

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に通知。

Page 62: Automated testing with Espresso2.x

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

Page 63: Automated testing with Espresso2.x

@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を登録する。

Page 64: Automated testing with Espresso2.x

@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を解除する。

Page 65: Automated testing with Espresso2.x

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

Page 66: Automated testing with Espresso2.x

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

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

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

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

Page 67: Automated testing with Espresso2.x

TestCase UI

TestCase

TestCaseonView()

onView()

onView()

Page 68: Automated testing with Espresso2.x

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

Page 69: Automated testing with Espresso2.x

@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操作を直接記述する。

Page 70: Automated testing with Espresso2.x

@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取得を直接記述する。

Page 71: Automated testing with Espresso2.x

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

Page 72: Automated testing with Espresso2.x

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

Page 73: Automated testing with Espresso2.x

TestCase PageObject

TestCase

TestCasefindLoadingView()

findLoadingView()

findLoadingView()

UIonView()

Page 74: Automated testing with Espresso2.x

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

Page 75: Automated testing with Espresso2.x

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操作をページオブジェクトに集約する。

Page 76: Automated testing with Espresso2.x

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取得もページオブジェクトに集約する。

Page 77: Automated testing with Espresso2.x

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

Page 78: Automated testing with Espresso2.x

@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操作はページオブジェクトを経由する。

Page 79: Automated testing with Espresso2.x

@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取得もページオブジェクトを経由する。

Page 80: Automated testing with Espresso2.x

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

Page 81: Automated testing with Espresso2.x

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

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

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

2

3

4

5 EspressoのAPIをラップする。

Sleepによる待機を避ける。

Page 82: Automated testing with Espresso2.x

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

Page 83: Automated testing with Espresso2.x

@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をクリックする。

Page 84: Automated testing with Espresso2.x

@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の可視性を検証する。

Page 85: Automated testing with Espresso2.x

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

Page 86: Automated testing with Espresso2.x

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

Page 87: Automated testing with Espresso2.x

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

Page 88: Automated testing with Espresso2.x

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を取得する。

Page 89: Automated testing with Espresso2.x

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の可視性を検証する。

Page 90: Automated testing with Espresso2.x

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

Page 91: Automated testing with Espresso2.x

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

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

Page 92: Automated testing with Espresso2.x

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

Page 93: Automated testing with Espresso2.x

まとめ

Page 94: Automated testing with Espresso2.x

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

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

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

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