Android Activityのテストを上手く書くコツ

はじめに

こんにちは!エウレカのテストおじさんこと、Androidエンジニアの海藤です。

 

今回はAndroidにおいてテストを書くために僕が取り組んでいることについて紹介したいと思います。今回ブログを書くにあたって、Androidのテスト全般について書こうと思いましたが、書いているうちにかなりの量になってしまったため、今回はActivityのテストに絞って書きたいと思います。

Android界隈のテストについて

Android界隈はサーバーサイド界隈と比較して、テストを書く文化が定着しているとは言えない状況です。もちろんちゃんとテストを書いてガンガンCIを回してるぜ、というところもあるかと思いますが、全体からみると少数派ではないでしょうか。

 

年々複雑化・高度化する要求に対して人力でテストをするのはもはや現実的ではありません。Android界隈では最近ようやくAndroid Testing Bootcampといった勉強会も開催されるようになり、徐々にちゃんとテストを書こうという流れが出来つつあるように思います。

Activityのテストを書く際の障害

早速本題に移りますが、Activityのテストを書く際にほぼ間違いなく直面する問題として、マッチョなActivity問題が挙げられます。

マッチョなActivity

AndroidではMVCを採用する場合が多いかと思いますが、AndroidにMVCを素直に適用した場合、各レイヤーの関係は以下のようになるかと思います。

android-activity-test-mvc

上記の図では、ActivityがViewとController両方の性質を持っています。

 

この状態ではViewとControllerの処理をActivityに書くことになるため、結果として非常に巨大なActivityが出来上がってしまいます。ViewとControllerだけならまだしも、Modelとしての処理も持ってしまっているActivityも世の中には存在していると聞きます。そして、このように様々な処理がつめ込まれたActivityをマッチョなActivityと呼びます。

 

ちなみにマッチョなActivityは、DroidKaigi 2015基調講演で登場した言葉です。

 

テストを書くという観点で考えても、1つのクラスに様々な処理が詰め込まれている状態は好ましくありません。また、そもそもActivityはAndroidフレームワークに依存したクラスで、Activityに書かれている時点でテストが若干面倒になるという問題もあります。

マッチョなActivityの倒し方

ActivityがViewとControllerとしての性質を持っていること自体は、Activityがそのように実装されているので正直どうしようもありません。そこでこのViewとContollerという2つの性質のうち、Activityから切り離しやすい方はどちらかということを考えてみます。

 

AndroidにおけるViewはActivityと同じライフサイクルで動作しており、基本的にはActivityから切り離すのは難しいと考えられます。一方でControllerは、ロジックが発火する起点となるコールバックメソッド自体はActivityに書く必要があるものの、その内部ロジックはActivityから比較的容易に切り離し可能です。

 

上記のような理由からActivityからControllerロジックを切り離すことで、ActivityがViewロジックに専念できることになります。このActivityからControllerロジックを切り離す方法としては「MVP」と呼ばれるアーキテクチャが一番有名かと思います。

MVP

AndroidでMVPを採用する場合は大体以下のような構成になるかと思います。

android-activity-test-mvp

MVPではActivityを完全にViewとして捉え、MVCにおけるControllerとしての処理をPresenterが担います。MVPにおけるModel、View、Presenterそれぞれの役割を整理すると以下のようになります。

  • Model
    • ModelはMVCと同様に所謂ビジネスロジックを担当します。もう少し具体的にいうと、APIやDBとのデータのやり取り、データの保存、データの加工といったものです。
  • View
    • ViewはControllerから受け取ったデータを画面に表示する、ユーザーからの入力イベントを受け取るといった部分を担当します。
  • Presenter
    • Viewから移譲されたイベントをもとにModelとやり取りを行って、Viewに対して適切なフィードバックを返すという部分を担当します。

このようにMVPで記述することで各レイヤーの責務が明確になり、それぞれに対するテストを書くことが可能になります。具体例については、次のサンプルをご覧ください。

サンプルアプリ

今回は以下のようなアプリのテストを書く場合を考えてみます。ちなみにこのアプリはDroidKaigi 2016公式アプリのContributorsを一覧表示するというものです。

android-activity-test-sample-app

このアプリの要件は以下の通りです。

  • 起動時にプログレスが表示される
  • 起動時に最新データの読み込みが行われる
  • 読み込み完了後にContributor一覧が表示される
  • 読み込みが完了したらプログレスを隠す

上記の要件をViewとPresenterそれぞれのタスクとして分解してみます。

View

  • プログレスを表示する
  • プログレスを隠す
  • Contributor一覧を表示する

Presenter

  • 起動時にプログレスを表示する
  • 最新データを読み込んでViewに渡す
  • 読み込み完了後にプログレスを隠す

Viewのテスト

プログレスを表示する・プログレスを隠す

この2つは基本的には同じことをやっているため、表示の場合のみ記載します。

実装

    @Override
    public void showProgressBar() {
        swipeRefreshLayout.setRefreshing(true);
    }

テスト

    @Test
    public void showProgressBarTest() {
        SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) view
                .findViewById(R.id.fragment_github_swipe_refresh_layout);
        // 最初はプログレスを非表示
        swipeRefreshLayout.setRefreshing(false);

        // 最初はプログレスが表示されていないことを確認
        assertThat(swipeRefreshLayout.isRefreshing(), is(false));

        // プログレスを表示
        githubFragment.showProgressBar();

        // プログレスが表示されていることを確認
        assertThat(swipeRefreshLayout.isRefreshing(), is(true));
    }

Contributor一覧を表示する

実装

    @Override
    public void setGithubContributors(List<GithubContributor> githubContributors) {
        githubAdapter.setGithubContributors(githubContributors);
    }

テスト

「Contributor一覧を表示する」という機能をテストするにあたって、データの出処は関係ないためダミーデータを作って、それが表示されているかというテストを行います。

    @Test
    public void setGithubContributorsTest() {
        ListView listView = (ListView) view.findViewById(R.id.fragment_github_list_view);

        // 最初は何も表示されていないことを確認
        assertThat(listView.getCount(), is(0));

        // ダミーデータを生成
        List<GithubContributor> githubContributors = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            GithubContributor githubContributor = new GithubContributor();
            githubContributor.login = String.valueOf(i);
            githubContributor.avatarUrl = String.valueOf(i);
            githubContributor.htmlUrl = String.valueOf(i);
            githubContributors.add(githubContributor);
        }

        // ダミーデータを設定
        githubFragment.setGithubContributors(githubContributors);

        // ダミーデータが表示されていることを確認
        assertThat(listView.getCount(), is(10));
    }

Presenterの実装

MVPにおいて、ViewとPresenterは1対1になり、画面の初期化時にPresenterの初期化も行うという流れになります。

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        githubPresenter = new GithubPresenter(getContext(), this, githubUseCase);
        githubPresenter.onCreate();
    }

そして、画面の初期化時に行うべき処理はPresenter#onCreate()に移譲されます。

    public void onCreate() {
        githubView.initViews();
        githubView.showProgressBar();
        githubView.refresh();
    }

また、クリックイベントなどのコールバックメソッドも全てPresenterに移譲することになります。コールバックが発火した際の処理をPresenterに移譲しておくことで、それらの処理を簡単にテストすることが可能になります。

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        githubPresenter.onItemClick(githubAdapter.getItem(position));
    }

Presenterのテスト

起動時にプログレスを表示する

実装

Presenter#onCreate()は起動時の処理が委譲されています。

    public void onCreate() {
        githubView.initViews();
        githubView.showProgressBar();
        githubView.refresh();
    }

テスト

verify()とtimes()という見慣れないメソッドがあるかと思いますが、これはMochitoというライブラリのメソッドで、以下のような機能があります。

  • verify():メソッドが呼びだされているかを確認するメソッド
  • times():メソッドが呼び出される回数を指定するメソッド
    @Test
    public void onCreateTest() {
        githubPresenter.onCreate();

        verify(githubView, times(1)).initViews();
        verify(githubView, times(1)).showProgressBar();
        verify(githubView, times(1)).refresh();
    }

最新データを読み込んでViewに渡す・読み込み完了後にプログレスバーを隠す

実装

    public void refresh() {
        githubUseCase.getGithubContributors()
                .subscribeOn(scheduler)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<List<GithubContributor>>() {
                    @Override
                    public void call(List<GithubContributor> githubContributors) {
                        githubView.setGithubContributors(githubContributors);
                        githubView.hideProgressBar();
                    }
                });
    }

テスト

ここで重要なのがPresenterとしての仕事はあくまで「最新データを読み込んでViewに渡す」ことで、データの出処がどこなのかという点に関してはPresenterは関与しないということです。今回の例でいうと、最新データの取得に関してはGithubUseCaseというクラスに移譲しています。こうしておくことで、PresenterではGithubUseCaseからデータを取得して、Viewに渡すという本来のロジックだけを書けば良くなります。

 

また、テストでは実際にAPIを叩いて最新のデータを取得する必要はないため、GithubUseCaseがダミーデータを返すようにして、そのダミーデータがきちんとViewに渡されているかというテストを行っています。このダミーデータを返すという部分はDIと呼ばれる仕組みを利用しても実現可能ですが、今回は説明を簡潔にするためにあえて使用していません。

    @Test
    public void refreshTest() {
        // ダミーデータを設定
        final List<GithubContributor> dummyGithubContributors = new ArrayList<>();
        Observable<List<GithubContributor>> observable = Observable.create(
                new Observable.OnSubscribe<List<GithubContributor>>() {
                    @Override
                    public void call(Subscriber<? super List<GithubContributor>> subscriber) {
                        subscriber.onNext(dummyGithubContributors);
                        subscriber.onCompleted();
                    }
                }
        );
        when(githubUseCase.getGithubContributors()).thenReturn(observable);

        githubPresenter.refresh();

        // Viewにデータが受け渡されていることを確認
        verify(githubView, times(1)).setGithubContributors(dummyGithubContributors);
        // プログレスが非表示になっていることを確認
        verify(githubView, times(1)).hideProgressBar();
    }

さいごに

本稿で登場したコードについては以下のリポジトリで公開しているので、興味のある方は覗いてみてください。

今回はActivityのテストに話題を絞って書きました。本文中でも述べた通り、Activityは何も考えずに実装してしまうと非常にテストが書きにくい状態となってしまいます。本稿で解説したアプローチでActivityを実装することである程度のユニットを書くことができるようになるかと思います。

 

今回はMVPにおけるViewとPresenterのテストについて書いたので、次回はModelのテストをどのように書くかという観点で書きたいと思います!

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

ClojureScript & Golangでチャットアプリを作ってみた

恋人専用SNSアプリCouplesに追加されたThinking of you機能の実装方法