1. はじめに
Rx Androidをなぜ使用するかについては、【翻訳】AsyncTask と AsyncTaskLoader を rx.Observable に置き換える – RxJava Android Patternsが詳しい。
Rx Androidは、非同期の処理に使えるが、Espresso UI testing with RxJavaの記事によると、記事を書いた人が開発したnovoda/rxpressoを利用すると、Rx Androidを使用したアプリのUIテストを容易に行うことができるそうなので、RxPressoを使用してみた。
本記事では、Slashdot JapanのモバイルニュースのRSSを取得して、リスト表示、リストの行をタップすると、ニュース詳細を見れるサンプルアプリを作成し、また、RxPressoとEspressを使用して、UIのテストを行った。
2. app/build.gradleの編集
dependenciesに次の行を追加します。
// Mockito Dependencies debugCompile('com.google.dexmaker:dexmaker-mockito:1.2') { exclude module: 'hamcrest-core' } debugCompile 'com.google.dexmaker:dexmaker:1.2' debugCompile('org.mockito:mockito-core:1.10.19') { exclude module: 'hamcrest-core' } // Espresso 2 Dependencies androidTestCompile('com.android.support.test.espresso:espresso-core:2.2') { exclude module: 'support-annotations' } androidTestCompile('com.android.support.test:runner:0.2') { exclude module: 'support-annotations' } androidTestCompile('com.android.support.test:rules:0.2') { exclude module: 'support-annotations' } compile('io.reactivex:rxandroid:0.23.0') { exclude module: 'rxjava' } compile 'io.reactivex:rxjava:1.0.10' androidTestCompile('com.novoda:rxpresso:0.1.5') { exclude module: 'support-annotations' }
また、このままだとビルドエラーが起きるので、androidブロックに、次の記述を追加します。
packagingOptions { exclude 'LICENSE.txt' }
3. 記事一つひとつに対するItemクラスの定義
記事一つひとつに対するItemクラスを定義します。
public class Item { // 記事のタイトル private CharSequence mTitle; // リンク private String mLink; // 記事の本文 private CharSequence mDescription; public Item() { mTitle = ""; mDescription = ""; } public CharSequence getDescription() { return mDescription; } public void setDescription(CharSequence description) { mDescription = description; } public CharSequence getTitle() { return mTitle; } public void setTitle(CharSequence title) { mTitle = title; } public String getLink() { return mLink; } public void setLink(String link) { mLink = link; } }
Slashdot JapanのRSSデータを見ると、title、link、descriptionタグがあるので、Itemクラスにフィールド、mTitle、mLink、mDescription を持たせ、セッター、ゲッターを定義しました。
4. ItemのListを持つクラスの定義
Rx AndroidのObservable.OnSubscribeクラスのコンストラクタは、引数に、パラメータ型を持てないようなので、List
public class RssData { List<Item> mItems; public void setItems(List<Item> items) { mItems = items; } public List<Item> getItems() { return mItems; } }
セッターとゲッターを定義しています。
5. Web APIを定義するinterfaceを作る
RSSデータ、RssDataを、メソッド名は、getRssDataとします。また、引数は、urlなので、String型とします。Rx Androidを使用するために、次のinterfaceを定義します。
import rx.Observable; public interface DataRepository { Observable<RssData> getRssData(String param); }
Rx Androidを使用するため、RssDataをObservableで包んでいます。
6. interfaceを実装する
5で定義したinterfaceを実装します。
import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import rx.Observable; import rx.Subscriber; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.List; public class ConcreteDataRepository implements DataRepository { @Override public Observable<RssData> getRssData(final String param) { return Observable.create( new Observable.OnSubscribe<RssData>() { @Override public void call(Subscriber<? super RssData> subscriber) { try { List<Item> result = null; URL url = new URL(param); InputStream is = url.openConnection().getInputStream(); result = parseXml(is); RssData rssData = new RssData(); rssData.setItems(result); subscriber.onNext(rssData); } catch (IOException e) { subscriber.onError(e); } catch (XmlPullParserException e) { subscriber.onError(e); } } } ); } // XMLをパースする public List<Item> parseXml(InputStream is) throws IOException, XmlPullParserException { List<Item> items = new ArrayList<Item>(); XmlPullParser parser = Xml.newPullParser(); try { parser.setInput(is, null); int eventType = parser.getEventType(); Item currentItem = null; while (eventType != XmlPullParser.END_DOCUMENT) { String tag = null; switch (eventType) { case XmlPullParser.START_TAG: tag = parser.getName(); if (tag.equals("item")) { currentItem = new Item(); } else if (currentItem != null) { if (tag.equals("title")) { currentItem.setTitle(parser.nextText()); } else if (tag.equals("link")) { currentItem.setLink(parser.nextText()); } else if (tag.equals("description")) { currentItem.setDescription(htmlTagRemover(parser.nextText())); } } break; case XmlPullParser.END_TAG: tag = parser.getName(); if (tag.equals("item")) { items.add(currentItem); } break; } eventType = parser.next(); } } catch (IOException e) { e.printStackTrace(); throw e; } catch (XmlPullParserException e) { throw e; } return items; } /** * HTMLタグ削除(すべて) * http://it--trick.appspot.com/article/30051/40001/70001/70002.html * * @param str 文字列 * @return HTMLタグ削除後の文字列 */ public static String htmlTagRemover(String str) { // 文字列のすべてのタグを取り除く return str.replaceAll("<.+?>", ""); } }
ここで、interfaceで定義したgetRssDataメソッドを次のように実装しています。
@Override public Observable<RssData> getRssData(final String param) { return Observable.create( new Observable.OnSubscribe<RssData>() { @Override public void call(Subscriber<? super RssData> subscriber) { try { List<Item> result = null; URL url = new URL(param); InputStream is = url.openConnection().getInputStream(); result = parseXml(is); RssData rssData = new RssData(); rssData.setItems(result); subscriber.onNext(rssData); } catch (IOException e) { subscriber.onError(e); } catch (XmlPullParserException e) { subscriber.onError(e); } } } ); }
Java 1.8を使うと、簡潔に書けるようですが、本記事では、Java 1.6での書き方を行っているため、コードが複雑になっていますが、Android StudioやIntelliJのコード補完で容易にコードを書くことができるはずです。このコードでは、paramで指定されたurlからInputStreamを取得し、Xmlでパースし、結果、List
parseXmlメソッドの実装は、第5回 RSSリーダーの要、パース機能を知るの記事を参考にしました。
7. Applicationを継承したクラスAppを定義する
テストクラスからも、Rx Androidを利用して定義したメソッドにアクセスできるように、Applicationクラスを継承したクラスAppを定義します。
import android.app.Application; import com.sarltokyo.sladmobilerssreader.data.ConcreteDataRepository; import com.sarltokyo.sladmobilerssreader.data.DataRepository; public class App extends Application { private static App sInstance; @Override public void onCreate() { super.onCreate(); sInstance = this; } public static App getInstance() { return sInstance; } private DataRepository repository = new ConcreteDataRepository(); public DataRepository getRepository() { return repository; } public void setRepository(DataRepository repository) { this.repository = repository; } }
プロダクションコード、テストコードの両方から、Appインスタンスを取得できるように、Appインスタンスを返すgetInstanceメソッドを定義しています。また、プロダクションコードで、Web APIの本番用実装を利用できるように、repository にConcreteDataRepositoryのインスタンスを入れます。プロダクションコードでは、FragmentのonActivityCreatedメソッド内で、
dataRepository = App.getInstance().getRepository();
とすることにより、dataRepositoryにConcreteDataRepositoryのインスタンスを入れ、テスト時には、setRepositoryメソッドを呼ぶことにより、テスト用のDataRepositoryを使用するようにします。
また、Manifestファイルに、このAppを登録しておきます。これを忘れると、アプリが落ちるし、テストもできません。あと、ネットワークにアクセスするので、パーミッションも追加しておきます。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.sarltokyo.sladmobilerssreader.app" > <uses-permission android:name="android.permission.INTERNET" /> <application android:name="com.sarltokyo.sladmobilerssreader.App" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
8. Fragmentから、Rx Androidを利用する
Fragmentから、Rx Androidを利用します。RssListFragmentクラスを次のように実装します。
import android.os.Bundle; import android.support.v4.app.ListFragment; import android.view.View; import android.widget.ListView; import android.widget.Toast; import com.sarltokyo.sladmobilerssreader.App; import com.sarltokyo.sladmobilerssreader.adapter.RssListAdapter; import com.sarltokyo.sladmobilerssreader.data.DataRepository; import com.sarltokyo.sladmobilerssreader.data.Item; import com.sarltokyo.sladmobilerssreader.data.RssData; import rx.Observer; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; import java.util.ArrayList; import java.util.List; public class RssListFragment extends ListFragment { private final static String TAG = RssListFragment.class.getSimpleName(); public static final String RSS_FEED_URL = "http://rss.rssad.jp/rss/slashdot/mobile.rss"; private List<Item> mItems; private RssListAdapter mAdapter; private Subscription subscription; private DataRepository dataRepository; // アイテムがタップされたときのリスナー public interface OnListItemClickListener { public void onListItemClick(int position, Item item); } // アイテムがタップされたときのリスナー private OnListItemClickListener mOnListItemClickListener; // アイテムがタップされたときのリスナーをセット public void setOnListItemClickListener(OnListItemClickListener l) { mOnListItemClickListener = l; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public void onDestroy() { super.onDestroy(); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); dataRepository = App.getInstance().getRepository(); // Itemオブジェクトを保持するためのリストを生成し、アダプタに追加する mItems = new ArrayList<Item>(); mAdapter = new RssListAdapter(getActivity(), mItems); // アダプタをリストビューにセットする setListAdapter(mAdapter); getRssDataByRxJava(RSS_FEED_URL); } // リストのアイテムがタップされたときに呼び出される @Override public void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); showDetail(position); } private void showDetail(int position) { // タップされたときのリスナーのメソッドを呼び出す if (mOnListItemClickListener != null) { Item item = (Item)(mAdapter.getItem(position)); mOnListItemClickListener.onListItemClick(position, item); } } protected void refreshRss() { getRssDataByRxJava(RSS_FEED_URL); } void setRssResult(RssData rssData) { mItems = rssData.getItems(); // todo: ダブリコード mAdapter = new RssListAdapter(getActivity(), mItems); setListAdapter(mAdapter); } void setRssError() { Toast.makeText(getActivity(), "error", Toast.LENGTH_LONG).show(); } void getRssDataByRxJava(String param) { subscription = dataRepository.getRssData(param) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new RssDataObserver()); } private class RssDataObserver implements Observer<RssData> { @Override public void onCompleted() { // nop } @Override public void onError(Throwable e) { setRssError(); } @Override public void onNext(RssData rssData) { setRssResult(rssData); } } }
RssListFragmentのonActivityCreatedメソッド内で、
dataRepository = App.getInstance().getRepository();
によって、プロダクション用ConcreteDataRepositoryインスタンスをdataRepoitoryに入れています。こうすることにより、6で定義した、プロダクション用getRssDataを呼ぶことができます。
Rx Androidを使用しているコードは、次の部分です。
void setRssResult(RssData rssData) { mItems = rssData.getItems(); // todo: ダブリコード mAdapter = new RssListAdapter(getActivity(), mItems); setListAdapter(mAdapter); } void setRssError() { Toast.makeText(getActivity(), "error", Toast.LENGTH_LONG).show(); } void getRssDataByRxJava(String param) { subscription = dataRepository.getRssData(param) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new RssDataObserver()); } private class RssDataObserver implements Observer<RssData> { @Override public void onCompleted() { // nop } @Override public void onError(Throwable e) { setRssError(); } @Override public void onNext(RssData rssData) { setRssResult(rssData); } }
getRssDataByRxJavaメソッド内で、
* subscribeOn(Schedulers.io()): タスクを実行したいスレッドが何か(Schedulers.io)
* observeOn(AndroidSchedulers.mainThread()): 結果を受け取るスレッドは何か(AndroidSchedulers.mainThread)
* subscribe: Operatorで加工した値を受け取る
RssDataObserverクラスのそれぞれのメソッドでは、データを加工します。例外の場合、onErrorで、setErrorメソッドを呼び、Toastを表示します。onNextでは、一つのの値が終わるたびに実行されるので、setRssResultメソッドが呼ばれ、本アプリでは、リスト更新がされます。
9. androidTest/javaにテストクラスを作る
androidTest/javaクラスに次のクラス SampleTest を作ります。
import android.os.IBinder; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.Espresso; import android.support.test.espresso.Root; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.view.WindowManager; import android.widget.ListView; import com.novoda.rxmocks.RxMocks; import com.novoda.rxmocks.SimpleEvents; import com.novoda.rxpresso.RxPresso; import com.sarltokyo.sladmobilerssreader.app.MainActivity; import com.sarltokyo.sladmobilerssreader.app.R; import com.sarltokyo.sladmobilerssreader.app.RssListFragment; import com.sarltokyo.sladmobilerssreader.data.DataRepository; import com.sarltokyo.sladmobilerssreader.data.Item; import com.sarltokyo.sladmobilerssreader.data.RssData; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; import java.util.ArrayList; import java.util.List; import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.*; import static com.novoda.rxmocks.RxExpect.any; import static com.novoda.rxmocks.RxExpect.anyError; import static org.hamcrest.Matchers.anything; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) public class SampleTest { private RxPresso rxPresso; private DataRepository mockedRepo; private final static int DUMMY_DATA_NUM = 10; private final static int DUMMY_REFRESH_DATA_NUM = 5; @Rule public ActivityTestRule<MainActivity> rule = new ActivityTestRule<MainActivity>(MainActivity.class) { @Override protected void beforeActivityLaunched() { App application = (App) InstrumentationRegistry.getTargetContext().getApplicationContext(); mockedRepo = RxMocks.mock(DataRepository.class); application.setRepository(mockedRepo); rxPresso = new RxPresso(mockedRepo); Espresso.registerIdlingResources(rxPresso); } @Override protected void afterActivityFinished() { super.afterActivityFinished(); Espresso.unregisterIdlingResources(rxPresso); rxPresso.resetMocks(); } }; @Test public void testSucces() throws Exception { // ダミーデータ作成 List<Item> items = createDummyItems(DUMMY_DATA_NUM); RssData mockRssData = mock(RssData.class); when(mockRssData.getItems()).thenReturn(items); MainActivity activity = rule.getActivity(); // 何を返すか rxPresso.given(mockedRepo.getRssData(RssListFragment.RSS_FEED_URL)) .withEventsFrom(SimpleEvents.onNext(mockRssData)) // テストを実行する前に、何のイベントを待っているか .expect(any(RssData.class)) .thenOnView(withId(android.R.id.list)) .check(matches(isDisplayed())); // リストの行数のテスト FragmentManager fragmentManager = activity.getSupportFragmentManager(); Fragment fragment = fragmentManager.findFragmentById(R.id.my_fragment); RssListFragment listFragment = (RssListFragment)fragment; final ListView listView = listFragment.getListView(); assertThat(listView.getCount(), is(DUMMY_DATA_NUM)); // リストの各行の内容表示のテスト for (int i = 0; i < DUMMY_DATA_NUM; i++) { onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(i) .onChildView(withId(R.id.item_title)) .check(matches(withText("dummy title" + i))); onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(i) .onChildView(withId(R.id.item_descr)) .check(matches(withText("dummy text" + i))); } } @Test public void testToDetail() throws Exception { // ダミーデータ作成 List<Item> items = createDummyItems(DUMMY_DATA_NUM); RssData mockRssData = mock(RssData.class); when(mockRssData.getItems()).thenReturn(items); rule.getActivity(); // 何を返すか rxPresso.given(mockedRepo.getRssData(RssListFragment.RSS_FEED_URL)) .withEventsFrom(SimpleEvents.onNext(mockRssData)) // テストを実行する前に、何のイベントを待っているか .expect(any(RssData.class)) .thenOnView(withId(android.R.id.list)) .check(matches(isDisplayed())); // 遷移先の画面の内容をテストする // RxPressoの使い方がわからず、Espresoでやる onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(0) .perform(click()); onView(withId(R.id.item_detail_title)).check(matches(withText("dummy title0"))); onView(withId(R.id.item_detail_link)).check(matches(withText("http://dummy0.com"))); onView(withId(R.id.item_detail_descr)).check(matches(withText("dummy text0"))); } @Test public void testRefresh() throws Exception { // ダミーデータ作成 List<Item> items = createDummyItems(DUMMY_DATA_NUM); RssData mockRssData = mock(RssData.class); when(mockRssData.getItems()).thenReturn(items); MainActivity activity = rule.getActivity(); // 何を返すか rxPresso.given(mockedRepo.getRssData(RssListFragment.RSS_FEED_URL)) .withEventsFrom(SimpleEvents.onNext(mockRssData)) // テストを実行する前に、何のイベントを待っているか .expect(any(RssData.class)) .thenOnView(withId(android.R.id.list)) .check(matches(isDisplayed())); // 更新用データ作成 List<Item> refreshItems = createDummyRefreshItems(DUMMY_REFRESH_DATA_NUM); RssData refreshMockRssData = mock(RssData.class); when(refreshMockRssData.getItems()).thenReturn(refreshItems); // 何を返すか rxPresso.given(mockedRepo.getRssData(RssListFragment.RSS_FEED_URL)) .withEventsFrom(SimpleEvents.onNext(refreshMockRssData)) // テストを実行する前に、何のイベントを待っているか .expect(any(RssData.class)) .thenOnView(withId(R.id.action_refresh)) .perform(click()); // リストの行数のテスト FragmentManager fragmentManager = activity.getSupportFragmentManager(); Fragment fragment = fragmentManager.findFragmentById(R.id.my_fragment); RssListFragment listFragment = (RssListFragment)fragment; final ListView listView = listFragment.getListView(); assertThat(listView.getCount(), is(DUMMY_REFRESH_DATA_NUM)); // リストの各行の内容表示のテスト for (int i = 0; i < DUMMY_REFRESH_DATA_NUM; i++) { onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(i) .onChildView(withId(R.id.item_title)) .check(matches(withText("dummy title" + (DUMMY_DATA_NUM +i)))); onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(i) .onChildView(withId(R.id.item_descr)) .check(matches(withText("dummy text" + (DUMMY_DATA_NUM + i)))); } } /** * ダミーデータ作成 * @param count * @return */ private List<Item> createDummyItems(int count) { List<Item> items = new ArrayList<Item>(); for (int i = 0; i < count; i++) { Item item = new Item(); item.setTitle("dummy title" + i); item.setLink("http://dummy" + i + ".com"); item.setDescription("dummy text" + i); items.add(item); } return items; } /** * 更新用データ作成 * @param count * @return */ private List<Item> createDummyRefreshItems(int count) { List<Item> refreshItems = new ArrayList<Item>(); for (int i = DUMMY_DATA_NUM; i < DUMMY_DATA_NUM + count; i++) { Item item = new Item(); item.setTitle("dummy title" + i); item.setLink("http://dummy" + i + ".com"); item.setDescription("dummy text" + i); refreshItems.add(item); } return refreshItems; } @Test public void testIOException() throws Exception { rule.getActivity(); // エラーでToastが表示されるかのテスト // 何を返すか rxPresso.given(mockedRepo.getRssData(RssListFragment.RSS_FEED_URL)) .withEventsFrom(SimpleEvents.<RssData>onError(new IOException("error"))) // テストを実行する前に、何のイベントを待っているか .expect(anyError(RssData.class, IOException.class)) .thenOnView(withText("error")).inRoot(isToast()) .check(matches(isDisplayed())); } /** * Matcher that is Toast window. * http://baroqueworksdevjp.blogspot.jp/2015/03/espressotoast.html */ public static Matcher<Root> isToast() { return new TypeSafeMatcher<Root>() { @Override public void describeTo(Description description) { description.appendText("is toast"); } @Override public boolean matchesSafely(Root root) { int type = root.getWindowLayoutParams().get().type; if ((type == WindowManager.LayoutParams.TYPE_TOAST)) { IBinder windowToken = root.getDecorView().getWindowToken(); IBinder appToken = root.getDecorView().getApplicationWindowToken(); if (windowToken == appToken) { // windowToken == appToken means this window isn't contained by any other windows. // if it was a window for an activity, it would have TYPE_BASE_APPLICATION. return true; } } return false; } }; } }
通常、Espressoを利用して、UIのテストを行う場合、テストクラスは、ActivityInstrumentationTestCase2を継承しますが、Rx Andoriidを使う非同期処理を持つアプリのUIのテストで、RxPressoを使う場合、テストクラスに、@RunWith(AndroidJUnit4.class) アノーテーションを付け、テストクラス内で、@Ruleで、
@Rule public ActivityTestRule<MainActivity> rule = new ActivityTestRule<MainActivity>(MainActivity.class) { @Override protected void beforeActivityLaunched() { App application = (App) InstrumentationRegistry.getTargetContext().getApplicationContext(); mockedRepo = RxMocks.mock(DataRepository.class); application.setRepository(mockedRepo); rxPresso = new RxPresso(mockedRepo); Espresso.registerIdlingResources(rxPresso); } @Override protected void afterActivityFinished() { super.afterActivityFinished(); Espresso.unregisterIdlingResources(rxPresso); rxPresso.resetMocks(); } };
のように記述します。beforeActivityLaunchedでは、(App) InstrumentationRegistry.getTargetContext().getApplicationContext()で、Appインスタンスを取得し、RxMocksのmockメソッドで、DataRepositoryのモックを取得、App#setRepositoryメソッドを呼ぶことにより、モックを利用する設定を行います。そして、RxPressoのインスタンスを取得して、EspressoのregisterIdlingResourcesにより、IdlingResourceを登録します。
また、afterActivityFinishedで、IdlingResourceの登録解除を行います。
10. テストの内容の説明
テストでは、実際にインターネットにアクセスしてRSSデータを取得すのではなく、次のコードの通り、RssDataをmockに置き換え、それのgetIemsメソッドがダミーデータを返すようにして、ダミーのデータで、UIのテストを行います。
// ダミーデータ作成 List<Item> items = createDummyItems(DUMMY_DATA_NUM); RssData mockRssData = mock(RssData.class); when(mockRssData.getItems()).thenReturn(items);
テスト項目は次の通りです。
* リストの行数のテスト。リストの各行の内容表示のテスト
* 記事詳細画面の内容のテスト
* 更新ボタンを押したら、リストが正しく更新されるかのテスト
リストの行数のテスト。リストの各行の内容表示のテストのコードは次の通りです。
@Test
public void testSucces() throws Exception {
// ダミーデータ作成
List
RssData mockRssData = mock(RssData.class);
when(mockRssData.getItems()).thenReturn(items);
MainActivity activity = rule.getActivity();
// 何を返すか
rxPresso.given(mockedRepo.getRssData(RssListFragment.RSS_FEED_URL))
.withEventsFrom(SimpleEvents.onNext(mockRssData))
// テストを実行する前に、何のイベントを待っているか
.expect(any(RssData.class))
.thenOnView(withId(android.R.id.list))
.check(matches(isDisplayed()));
// リストの行数のテスト
FragmentManager fragmentManager = activity.getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.my_fragment);
RssListFragment listFragment = (RssListFragment)fragment;
final ListView listView = listFragment.getListView();
assertThat(listView.getCount(), is(DUMMY_DATA_NUM));
// リストの各行の内容表示のテスト
for (int i = 0; i < DUMMY_DATA_NUM; i++) {
onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(i)
.onChildView(withId(R.id.item_title))
.check(matches(withText("dummy title" + i)));
onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(i)
.onChildView(withId(R.id.item_descr))
.check(matches(withText("dummy text" + i)));
}
}
[/java]
RxPressoを使用する場合、何を返すか
[java]
// 何を返すか
rxPresso.given(mockedRepo.getRssData(RssListFragment.RSS_FEED_URL))
.withEventsFrom(SimpleEvents.onNext(mockRssData))
[/java]
と、テストをする前に、何のイベントを待っているか
[java]
// テストを実行する前に、何のイベントを待っているか
.expect(any(RssData.class))
.thenOnView(withId(android.R.id.list))
.check(matches(isDisplayed()));
[/java]
のように記述します。ここでは、
* mockedRepo.getRssData()で、データを取得したら、mocksRssDataを返す
* RssDataのデータが戻り、android.R.id.listのidのViewが表示されるイベント待つ
としています。
RxPressoで、リストの行数や、各行の内容のテストを行いたかったのですが、チェインさせる方法がわからなかったので、Espressoを使ったテストコードを書きました。Espresso UI testing with RxJavaによると、RxPressoはEspressoと併用できるそうなので、問題ありません。
// リストの行数のテスト
FragmentManager fragmentManager = activity.getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.my_fragment);
RssListFragment listFragment = (RssListFragment)fragment;
final ListView listView = listFragment.getListView();
assertThat(listView.getCount(), is(DUMMY_DATA_NUM));
// リストの各行の内容表示のテスト
for (int i = 0; i < DUMMY_DATA_NUM; i++) {
onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(i)
.onChildView(withId(R.id.item_title))
.check(matches(withText("dummy title" + i)));
onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(i)
.onChildView(withId(R.id.item_descr))
.check(matches(withText("dummy text" + i)));
}
[/java]
Fragmentを使ったアプリなので、コードの通り、FragmentManagersインスタンス、RssListFragmentインスタンスを取得後、ListFragment#getListFragmentメソッドで、listViewを取得して、リストの行数をテストしています。また、Espressoの流儀で、各行の内容のテストを行っています。
残りのテスト項目も同様にコーディングして、テストを行います。
11. サンプルプロジェクト公開
今回作成したサンプルプロジェクトをGitHubに公開します。
andropenguin/SladMobileRssReader
12. 参考サイト
Espresso UI testing with RxJava
vladlichonos/Android-Examples
第4回 簡単なRSSリーダーを作ってみる
ReactiveX/RxAndroid
android-test-kit
novoda/rxpresso