【Android】Espressoでの非同期テストでIdlingResourceを使用する

Espressoで非同期テストのやり方を学んだのでメモ

## IdlingResource

https://developer.android.com/training/testing/espresso/idling-resource?hl=ja

Espressoで非同期な処理を使用したUIテストを実施するときは、安易にsleepとか使用せずに、`IdlingResource`を使うことが推奨されているようです。

`IdlingResource`自体はインタフェースなので、実際には`IdlingResource`を実装したクラスが必要になりますが、標準でいくつか用意されており今回は一番スタンダードな`CountingIdlingResource`を使用します。

>アクティブなタスクのカウンタを保持します。カウンタがゼロの場合、関連するリソースはアイドリング状態であると見なされます。この機能は、Semaphore の機能によく似ています。テスト中にアプリの非同期操作を管理するには、通常はこの実装で十分です。

## アイドルリソース使用の流れ
アイドルリソース使用の流れは以下になります。

1. テスト前にアイドルリソースを登録する
2. 非同期処理開始前にアイドルリソースがビジーであることをセットする
3. 非同期処理完了時点でアイドルリソースがアイドルであることをセットする
4. テスト終了後にアイドルリソースを登録解除する。

一般的には、1は`@Before`で実施し、4は`@After`で実施するようです。

## テスト設計
今回やりたいテストの流れについて設計します。

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472695/96492280-cead-be33-79d6-c77403f211e3.png)

「ProgressBar」「include」「WebView」の各コンポーネントが初期表示時は「Progress」のみ表示されていて、ページ読み込み完了し、Webページが表示できる直前で「WebView」のみ表示されるようになるよね？といった部分を確認するテストになります。

今回は、このうち「ページ読み込み完了」という部分が非同期でいつ完了になるのかわかりません。よって、アイドルリソースを使って「読み込み完了したよ！」ということをEspressoに教えてあげます。

## テスト実装
設計に基づいて実装をおこないます。

まず、テスト対象のフラグメントに手を加えます。

```java
public class TosViewFragment extends Fragment implements AppWebViewClientCallback {

    @VisibleForTesting
    private final CountingIdlingResource countingIdlingResource
            = new CountingIdlingResource("WebViewIdlingResource");

    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    public IdlingResource getCountingIdlingResource() {
        return countingIdlingResource;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        // ビジー状態
        countingIdlingResource.increment();
        // 省略
        // Viewの返却
        return binding.getRoot();
    }

    @Override
    public void onPageCommitVisibleHandle(boolean isOk) {
        // プログレスバーを非表示にする
        binding.scTovPbProgressBar.setVisibility(View.GONE);
        if (isOk) { // ページ表示可
            // Webページを表示する
            binding.scTovWvTos.setVisibility(View.VISIBLE);
        } else {
            // Sorryページを表示する
            binding.scTovInSorry.setVisibility(View.VISIBLE);
        }
        // アイドル状態
        countingIdlingResource.decrement();
    }
}
```

👉 `CountingIdlingResource`のインスタンスを対象のフラグメントに生成します。（ゲッターとかはテストクラスからしか呼ばれないので`@VisibleForTesting(otherwise = VisibleForTesting.NONE)`をつけています。）

インスタンスを生成するときに、リソース名を引数にします。

つぎに、`onCreateView`でビジー状態にしています。`CountingIdlingResource`は内部でカウンタを持っており、ゼロになるとアイドル状態とみなされるので`#increment`でカウンタをインクリメント（つまり、ビジー状態）にします。

最後に、`onPageCommitVisibleHandle`（`WebViewClient#onPageCommitVisible`が呼ばれた時のコールバック処理）の最後で`#decrement`してあげて差し引き0（つまりアイドル状態）にしてあげます。


次にテストクラスに手を加えます。私はページオブジェクトが好きなので、ページオブジェクトで実装しています。

```java
@LargeTest
@RunWith(AndroidJUnit4.class)
public class ScT_ScTov {
    public ActivityScenario<MainActivity> scenario;
    private IdlingResource idlingResource;
    @Before
    public void setUp() {
        // アクティビティの起動
        scenario = ActivityScenario.launch(MainActivity.class);
    }
    @After
    public void tearDown() {
        // アイドルリソースの登録解除
        IdlingRegistry.getInstance().unregister(idlingResource);
    }
    @Test
    public void ScreenTest_ScTov() {
        startScreenTest()
                .isNotDisplayedScTovWbTos()         // WebViewが表示されていないこと
                .isDisplayedScTovPbProgressBar()    // ProgressBarが表示されていること
                .isNotDisplayedScTovInSorry();      // includeが表示されていないこと
                registerIdlingResource()            // アイドルリソースを登録
                .isDisplayedScTovWbTos()            // WebViewが表示されていること
                .isNotDisplayedScTovPbProgressBar() // ProgressBarが表示されていないこと
                .isNotDisplayedScTovInSorry()       // includeが表示されていること
        ;
    }
    @NonNull
    private ScTovPO startScreenTest() {
        return new ScManPO()
                .clickHamburgerMenu()       // ハンバーガーメニュー_押下
                .clickTosViewMenuItem();    // 利用規約・免責事項メニューアイテム_押下
    }
    private ScTovPO registerIdlingResource() {
        scenario.onActivity(activity -> {
            // TosViewFragment取得
            TosViewFragment tosViewFragment =
                    (TosViewFragment) Objects.requireNonNull(activity
                            .getSupportFragmentManager()
                            .findFragmentById(R.id.sc_man_fc_nav_host))
                    .getChildFragmentManager()
                    .getFragments()
                    .get(0);
            // アイドルリソース取得
            idlingResource = tosViewFragment.getCountingIdlingResource();
            // アイドルリソースを登録
            IdlingRegistry.getInstance().register(idlingResource);
        });
        return new ScTovPO();
    }
}
```

👉 今回色々試してみましたが、上記のような形で成功しました。

処理の流れは以下の通りです。

1. setUpでアクティビティを起動
2. ScreenTest_ScTovでUIテスト開始
3. startScreenTestでアクティビティから対象画面への遷移操作を実施
4. ScreenTest_ScTovの途中で「アイドルリソースの登録」を行う。
5. アイドルリソースをフラグメントから取得し登録する。
6. テスト終了後、tearDownでアイドルリソースの登録解除


4,5は`#registerIdlingResource`でやっています。アクティビティからフラグメントを取得し、ゲッターを介して取得したものをレジストしています。

## テスト評価
Espressoを使ってUIテストを実施します。

```bash
Testing started at 10:04 PM ...
04/12 22:04:00: Launching 'ScreenTest_ScTov()' on Pixel 2 API 28 (SmartPhone).
App restart successful without re-installing the following APK(s): androidx.test.orchestrator, androidx.test.services
Running tests
```

無事テストが成功したことがわかります。

 

IdlingResourceを使えば、Espressoでの非同期テストも簡単に評価することができました。

めでたしめでたし。