AsyncTaskLoaderを使ったアドレス帳アプリで、Google Dagger 2を使って、テスト時にDBファイルをテスト用ファイルに置き換える

1. はじめに

以前、記事、ORMLiteを使ったアドレス帳アプリをテストしやすいように書き換え、Espressoでテストしたで、アドレス帳アプリの各種テストのやり方を書きました。その記事では、住友孝郎@cattaka_net氏のスライド開発を効率的に進めるられるまでの道程で紹介されているように、テスト時、データベースファイルを、RenamingDelegatingContextを使って、テスト用ファイルに置き換えて、各種テストを行いました。その方法は有効ではあるのですが、データベースからデータを読み込み、リスト画面にデータを表示するのに、Rx Androidを使うアプリの場合、Espressoを使ったUIのテスト時、ListViewの表示終了を待ってくれず、テストがうまくできませんでした。そこで、非同期処理には、Rx Androidの使用を断念して、従来のAsyncTaskLoaderを使用する方針に転換しました。Google Dagger 2を使ったUIテストについては、Google Dagger 2を使ってSharedPreferencesのテストを行ってみたや、AsyncTaskLoaderを使ったアプリで、Google Dagger 2とEspressoを使ってUIテストをするの記事で、SharedPreferencesやAsyncTaskLoaderが絡むUIのテストはやったのですが、Google Dagger 2をデータベースに適用して、データベースの絡むUIテストはまだやっていませんでした。RenamingDelegatingContextを使えば、依存性注入をやることもないのですが、SharedPreferencesやネットワークアクセスAPIに依存性注入をやるようなアプリでは、データベースにも依存性注入をやって統一するのもいいのではないかと思います。

2. テスト対象アプリ

テスト対象アプリは、andropenguin/AddressBookORMLite7ですが、画面遷移に、startActivityを使うのをやめ、Fragmentを使用することにしました。また、以前のアプリでは、データ操作に必要なSQLiteOpenHelperを継承したクラスOpenHelperのインスタンスを得るために、住友孝郎@cattaka_net氏の、FastCheckList/app/src/main/java/net/cattaka/android/fastchecklist/core/ にあるContextLogicとContextLogicFactoryを使用していましたが、RenamingDelegatingContextを使用せず、Google Dagger 2による依存性注入を行う方針なので、次のクラスOpenHelperで定義されるコンストラクタ OpenHelper(Context, String)で取得することにしました。

OpenHelper

public class OpenHelper extends OrmLiteSqliteOpenHelper {
    private final static String TAG = OpenHelper.class.getSimpleName();

    private final static int DATABASE_VERSION = 1;

    private Dao<Person, Integer> mPersonDao;
    private Dao<Address, Integer> mAddressDao;

    public OpenHelper(Context context, String name) {
        super(context, name, null, DATABASE_VERSION);
    }

   ...
}

ActivityとFragmentクラスは次のとおりです。コンストラクタOpenHelperの第2引数には、mDatabaseNameを渡し、MainActivityでは、それは、DATABASE_NAMEであり、PersonListFragmentや、RegisterFragmentでは、MainActivity.DATABASE_NAMEです。

MainActivity

public class MainActivity extends AppCompatActivity
        implements PersonListFragment.OnListItemClickListener {

    private final static String TAG = MainActivity.class.getSimpleName();

    private OpenHelper mOpenHelper;
    
    ...	

    public final static String DATABASE_NAME = "addressbook.db";

    public String mDatabaseName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mDatabaseName = DATABASE_NAME;

        mOpenHelper = new OpenHelper(this, mDatabaseName);

        ...
        }
    }
    ...
}

PersonListFragment

public class PersonListFragment extends ListFragment
        implements LoaderManager.LoaderCallbacks<List<Person>> {

    ...

    public String mDatabaseName;

    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mDatabaseName = MainActivity.DATABASE_NAME;

        mOpenHelper = new OpenHelper(getActivity(), mDatabaseName);
    }

    ...

    @Override
    public Loader<List<Person>> onCreateLoader(int id, Bundle args) {
        return new PersonLoader(getActivity(), mDatabaseName);
    }

    ...
}

RegisterFragment

public class RegisterFragment extends Fragment implements View.OnClickListener {

    ...

    public String mDatabaseName;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mDatabaseName = MainActivity.DATABASE_NAME;

        mOpenHelper = new OpenHelper(getActivity(), mDatabaseName);
    }
    
    ...
}

AsyncTaskLoaderを継承したクラス PersonLoaderのコンストラクタは、以前のアプリでは、

    public PersonLoader(Context context) {
        super(context);
        mContextLogic = ContextLogicFactory.createContextLogic(context);
        mOpenHelper = mContextLogic.createOpenHelper();
    }

になっていましたが、今回は、ContextLogicFactoryやContextLogicを使わないので、

    public PersonLoader(Context context, String databaseName) {
        super(context);

        mOpenHelper = new OpenHelper(context, databaseName);
    }

に書き換えました。

3. 依存性を見つける

今回は、依存性注入を行い、テスト時に、データベースファイルをテスト用に置き換えたいので、MainActivity、PersonListFragment、RegisterFragmentのmDatabaseNameがStringに依存していることから、mDatabaseNameに依存性注入を行うことにしました。

4. モジュールクラス

プロダクションコードのモジュールクラスは次のとおりです。ここでは、provideDatabaseNameメソッドは、実際のデータベースファイルのファイル名を返します。

DatabaseNameModule

@Module
public class DatabaseNameModule {

    DatabaseNameModule() {
    }

    @Provides
    @Singleton
    String provideDatabaseName() {
        return MainActivity.DATABASE_NAME;
    }
}

テスト用のモジュールクラスは次のとおりです。provideDatabaseNameメソッドは、mockModeがtrueの時は、プロダクション用のデータベースファイル名に、プレフィックス test_ を付けたファイル名を返すようにして、mockModeがfalseの時は、実際のデータベースファイル名を返すようにしました。

DebugDatabaseNameModule

@Module
public class DebugDatabaseNameModule {
    private final boolean mockMode;

    public DebugDatabaseNameModule(boolean provideMocks) {
        mockMode = provideMocks;
    }

    @Provides
    @Singleton
    String provideDatabaseName() {
        if (mockMode) {
            return "test_" + MainActivity.DATABASE_NAME;
        } else {
            return MainActivity.DATABASE_NAME;
        }
    }
}

5. @Injectを付け、injectを呼ぶ

次のように、MainActivity、PersonListFragment、RegisterFragmentクラスで、mDatabaseNameに、@Injectアノーテーションを付け、また、mDatabaseNameへの代入文をコメントアウトして、injectを呼びます。

MainActivity

public class MainActivity extends AppCompatActivity
        implements PersonListFragment.OnListItemClickListener {

    ...

    private OpenHelper mOpenHelper;

    ...

    public final static String DATABASE_NAME = "addressbook.db";

    @Inject
    public String mDatabaseName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

//        mDatabaseName = DATABASE_NAME;
        App.getInstance().component().inject(this);

        mOpenHelper = new OpenHelper(this, mDatabaseName);
        
        ...
     }
     
     ...
}

PersonListFragment

public class PersonListFragment extends ListFragment
        implements LoaderManager.LoaderCallbacks<List<Person>> {

    ...

    private OpenHelper mOpenHelper;

    @Inject
    public String mDatabaseName;

    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

//        mDatabaseName = MainActivity.DATABASE_NAME;
        App.getInstance().component().inject(this);

        mOpenHelper = new OpenHelper(getActivity(), mDatabaseName);
    }

    ...
}

RegisterFragment


public class RegisterFragment extends Fragment implements View.OnClickListener {
    private final static String TAG = RegisterFragment.class.getSimpleName();

    private OpenHelper mOpenHelper;
  
    ...

    @Inject
    public String mDatabaseName;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

//        mDatabaseName = MainActivity.DATABASE_NAME;
        App.getInstance().component().inject(this);
        
        mOpenHelper = new OpenHelper(getActivity(), mDatabaseName);
    }
    
    ...
}

6. テスト用のベースinjectedクラスを作る

InstrumentationTestCaseを使うクラスのために、debug/java配下に、次のクラスを作ります。

InstrumentationTestCase

public class InjectedInstrumentationTest extends InstrumentationTestCase {
    private final static String TAG = InjectedInstrumentationTest.class.getSimpleName();

    @Inject
    public String mockDatabaseName;

    public OpenHelper mOpenHelper;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        App app =
                (App)getInstrumentation().getTargetContext().getApplicationContext();
        app.setMockMode(true);
        app.component().inject(this);

        Context context = getInstrumentation().getTargetContext();
        mOpenHelper = new OpenHelper(context, mockDatabaseName);

        cleanData();
    }

    @Override
    protected void tearDown() throws Exception {
        App.getInstance().setMockMode(false);
        mOpenHelper.close();
        mOpenHelper = null;
    }

    public void cleanData() {
        Log.d(TAG, "cleanData");
        List<Person> persons = null;
        try {
            persons = mOpenHelper.findPerson();
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
        }

        for (Person person : persons) {
            try {
                mOpenHelper.deletePerson(person.getName());
            } catch (SQLException e) {
                Log.e(TAG, e.getMessage());
            }
        }
    }

    public List<Person> createTestData(int personsNum) {
        Log.d(TAG, "createTestData");
        List<Person> persons = new ArrayList<Person>();
        // Creating dummy data.
        for (int i = 0; i < personsNum; i++) {
            Person person = new Person();
            Address address = new Address();
            address.setZipcode("123-456" + i);
            address.setPrefecture("Tokyo");
            address.setCity("Shinjyuku-ku");
            address.setOther("Higashi-shinjyuku 1-2-" + i);
            person.setName("Hoge" + i);
            person.setAddress(address);
            try {
                mOpenHelper.registerPerson(person);
                persons.add(person);
            } catch (SQLException e) {
                Log.e(TAG, e.getMessage());
            }
        }
        return persons;
    }
}
&#91;/java&#93;

mockDatabaseNameに、アノーテーション @Injectを付け、

&#91;java&#93;
        App app =
                (App)getInstrumentation().getTargetContext().getApplicationContext();
        app.setMockMode(true);
        app.component().inject(this);

        Context context = getInstrumentation().getTargetContext();
        mOpenHelper = new OpenHelper(context, mockDatabaseName);

        cleanData();
&#91;/java&#93;

のように、後述で定義するAppクラスのインスタンスを取得して、setMockModeで、モックモードにして、injectで依存性注入を行います。そして、OpenHelperコンストラクタの第二引数に、依存性注入されたmockDatabaseNameを渡します。また、上記コードのように、データベースをクリアするcleanDataメソッドと、ダミーデータをデータベースに作るcreateTestDataメソッドを定義しています。

ActivityInstrumentationTestCase2を使うテストのために、debug/java配下に、次のクラスを作ります。

InjectedBaseActivityTest
&#91;java&#93;
public class InjectedBaseActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
    private final static String TAG = InjectedInstrumentationTest.class.getSimpleName();

    @Inject
    String mockDatabaseName;

    public OpenHelper mOpenHelper;

    public InjectedBaseActivityTest() {
        super(MainActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        App app =
                (App)getInstrumentation().getTargetContext().getApplicationContext();
        app.setMockMode(true);
        app.component().inject(this);

        Context context = getInstrumentation().getTargetContext();
        mOpenHelper = new OpenHelper(context, mockDatabaseName);

        cleanData();
    }

    @Override
    protected void tearDown() throws Exception {
        App.getInstance().setMockMode(false);
    }

    public void cleanData() {
        Log.d(TAG, "cleanData");
        List<Person> persons = null;
        try {
            persons = mOpenHelper.findPerson();
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
        }

        for (Person person : persons) {
            try {
                mOpenHelper.deletePerson(person.getName());
            } catch (SQLException e) {
                Log.e(TAG, e.getMessage());
            }
        }
    }

    public List<Person> createTestData(int personsNum) {
        Log.d(TAG, "createTestData");
        List<Person> persons = new ArrayList<Person>();
        // Creating dummy data.
        for (int i = 0; i < personsNum; i++) {
            Person person = new Person();
            Address address = new Address();
            address.setZipcode("123-456" + i);
            address.setPrefecture("Tokyo");
            address.setCity("Shinjyuku-ku");
            address.setOther("Higashi-shinjyuku 1-2-" + i);
            person.setName("Hoge" + i);
            person.setAddress(address);
            try {
                mOpenHelper.registerPerson(person);
                persons.add(person);
            } catch (SQLException e) {
                Log.e(TAG, e.getMessage());
            }
        }
        return persons;
    }
}
&#91;/java&#93;

依存性注入の仕方は、InjectedInstrumentationTestクラスと同様です。当初、InjectedBaseActivityTestクラスを、

&#91;java&#93;
public class InjectedBaseActivityTest<T extends Activity> extends ActivityInstrumentationTestCase2<T> {
    ...
}

のように、ジェネリックを使って定義したのですが、

Dagger not able to inject into InjectedBaseActivityTest #3

に、issueが上がっている通り、Google Dagger 2は、タイプパラメータを持ったInjectedBaseActivityTestに依存性注入ができないようです。そこで、androidTest/java配下に、同じ内容のクラスInjectedBaseActivityTestを作ってみたのですが、後述のコンポネントクラスから、そのクラスInjectedBaseActivityTestが見えないようで、その方法を断念しました。

また、InjectedBaseActivityTestクラスでも、データベースのデータをクリアするメソッドと、ダミーデーをデータベースに作るメソッドを定義しました。InjectedInstrumentationTestとコードのダブリがあるので、2つのメソッドをユーティリティクラスに抽出する試みをやったのですが、問題が生じてうまくいきませんでした。注意深くやればいいのかもしれませんが、それは宿題にします。

7. Applicationを継承したAppクラスを作る

Google Dagger 2を使ってSharedPreferencesのテストを行ってみたや、AsyncTaskLoaderを使ったアプリで、Google Dagger 2とEspressoを使ってUIテストをすると同様に、Applicationを継承したクラスAppを作り、マニフェストファイルに登録します。

App

public class App extends Application {

    private static App sInstance;
    private DatabaseNameComponent component;

    @Override
    public void onCreate() {
        super.onCreate();

        sInstance = this;
        component = DatabaseNameComponent.Initializer.init(false);
    }

    public static App getInstance() {
        return sInstance;
    }

    public DatabaseNameComponent component() {
        return component;
    }

    public void setMockMode(boolean useMock) {
        component = DatabaseNameComponent.Initializer.init(useMock);
    }
}

8. コンポネントクラス

コンポネントクラスは次のとおりです。

DatabaseNameComponent

@Singleton
@Component(modules = {DebugDatabaseNameModule.class})
public interface DatabaseNameComponent {

    void inject(MainActivity activity);
    void inject(PersonListFragment personListFragment);
    void inject(RegisterFragment registerFragment);
    void inject(InjectedBaseActivityTest test);
    void inject(InjectedInstrumentationTest instrumentationTest);

    public final static class Initializer {
        public static DatabaseNameComponent init(boolean mockMode) {
            return DaggerDatabaseNameComponent.builder()
                    .debugDatabaseNameModule(new DebugDatabaseNameModule(mockMode))
                    .build();
        }
    }
}

MainActivity、PersonListFragment、RegisterFragment、InjectedBaseActivityTest、InjectedInstrumentationTestの各クラスで、依存性注入を行うので、上記コードの通り、injectメソッドを定義します。

9. 各種テストクラス

9.1 データベースのテスト

上で定義したInjectedInstrumentationTestクラスを継承した、次の テストクラスを作ります。

OpenHelperTest

public class OpenHelperTest extends InjectedInstrumentationTest {

    public void testInsertSelect()throws Exception{
        assertEquals(0, mOpenHelper.findPerson().size());

        Person person = new Person();
        Address address = new Address();
        address.setZipcode("123-4567");
        address.setPrefecture("Tokyo");
        address.setCity("Shinjyuku-ku");
        address.setOther("Higashi-shinjyuku 1-2-3");
        person.setName("Foo");
        person.setAddress(address);
        // insert
        mOpenHelper.registerPerson(person);
        // select
        Person sut = mOpenHelper.findPerson("Foo");

        assertThat(sut.getAddress().getZipcode(), is("123-4567"));
        assertThat(sut.getAddress().getPrefecture(), is("Tokyo"));
        assertThat(sut.getAddress().getCity(), is("Shinjyuku-ku"));
        assertThat(sut.getAddress().getOther(), is("Higashi-shinjyuku 1-2-3"));
    }

    public void testDuplicate() throws  Exception {
        assertEquals(0, mOpenHelper.findPerson().size());

        Person person = new Person();
        Address address = new Address();
        address.setZipcode("123-4567");
        address.setPrefecture("Tokyo");
        address.setCity("Shinjyuku-ku");
        address.setOther("Higashi-shinjyuku 1-2-3");
        person.setName("Foo");
        person.setAddress(address);
        // insert
        mOpenHelper.registerPerson(person);
        assertEquals(1, mOpenHelper.findPerson().size());

        Person person2 = new Person();
        Address address2 = new Address();
        address.setZipcode("111-1111");
        address.setPrefecture("Kyoto");
        address.setCity("Kyoto");
        address.setOther("boo 1-2-3");
        person2.setName("Foo"); // duplicate
        person2.setAddress(address2);
        // insert
        // JUnit 3の書き方になってしまう
        try {
            mOpenHelper.registerPerson(person2);
            fail("SQLException is expected");
        } catch (SQLException e) {
        }

        assertEquals(1, mOpenHelper.findPerson().size());
    }

    public void testRegister() throws Exception {
        assertEquals(0, mOpenHelper.findPerson().size());

        // insert
        createTestData(200);

        // select
        List<Person> sut = mOpenHelper.findPerson();
        assertEquals(200, sut.size());
    }

    public void testFind() throws Exception {
        assertEquals(0, mOpenHelper.findPerson().size());

        Person person = new Person();
        Address address = new Address();
        address.setZipcode("123-4567");
        address.setPrefecture("Tokyo");
        address.setCity("Shinjyuku-ku");
        address.setOther("Higashi-shinjyuku 1-2-3");
        person.setName("Foo");
        person.setAddress(address);
        // insert
        mOpenHelper.registerPerson(person);
        // select
        List<Person> sut = mOpenHelper.findPerson();

        assertEquals(1, sut.size());

        assertThat(sut.get(0).getName(), is("Foo"));
        assertThat(sut.get(0).getAddress().getZipcode(), is("123-4567"));
        assertThat(sut.get(0).getAddress().getPrefecture(), is("Tokyo"));
        assertThat(sut.get(0).getAddress().getCity(), is("Shinjyuku-ku"));
        assertThat(sut.get(0).getAddress().getOther(), is("Higashi-shinjyuku 1-2-3"));
    }

    public void testUpdate() throws Exception {
        assertEquals(0, mOpenHelper.findPerson().size());

        Person person = new Person();
        Address address = new Address();
        address.setZipcode("123-4567");
        address.setPrefecture("Tokyo");
        address.setCity("Shinjyuku-ku");
        address.setOther("Higashi-shinjyuku 1-2-3");
        person.setName("Foo");
        person.setAddress(address);
        // insert
        mOpenHelper.registerPerson(person);
        // select
        List<Person> sut = mOpenHelper.findPerson();

        assertEquals(1, sut.size());

        Person person2 = mOpenHelper.findPerson(person.getName());
        Address address2 = person2.getAddress();
        address2.setZipcode("123-0000");
        address2.setPrefecture("Osaka");
        address2.setCity("Osaka");
        address2.setOther("hoge 1-2-3");
        person2.setName("Foo2");
        person2.setAddress(address2);
        // update
        mOpenHelper.updatePerson(person2);
        // select
        assertEquals(1, mOpenHelper.findPerson().size());
        Person sut2 = mOpenHelper.findPerson("Foo2");
        assertThat(sut2.getAddress().getZipcode(), is("123-0000"));
        assertThat(sut2.getAddress().getPrefecture(), is("Osaka"));
        assertThat(sut2.getAddress().getCity(), is("Osaka"));
        assertThat(sut2.getAddress().getOther(), is("hoge 1-2-3"));
        assertThat(sut2.getName(), is("Foo2"));
    }

    public void testDelete() throws SQLException {
        assertEquals(0, mOpenHelper.findPerson().size());

        Person person = new Person();
        Address address = new Address();
        address.setZipcode("123-4567");
        address.setPrefecture("Tokyo");
        address.setCity("Shinjyuku-ku");
        address.setOther("Higashi-shinjyuku 1-2-3");
        person.setName("Foo");
        person.setAddress(address);
        // insert
        mOpenHelper.registerPerson(person);
        // delete
        mOpenHelper.deletePerson(person.getName());
        assertEquals(0, mOpenHelper.findPerson().size());
    }
}

mOpenHelperは、継承元のInjectedInstrumentationTestにあるフィールドで、InjectedInstrumentationTestクラスでは、

        Context context = getInstrumentation().getTargetContext();
        mOpenHelper = new OpenHelper(context, mockDatabaseName);

であったので、テスト用データベースファイルを使ってテストを行っています。テスト項目は次のとおりです。

* データを1件データベースに挿入して、データをデータベースから取得したら、入力したものになっているか
* nameがダブっているデータをデータベースに挿入しようとすると、SQLExceptionが発生するか
* データベースに200件データを挿入して、全件取得したら、データ数は200件になっているか
* データベースにデータを1件挿入して、nameでデータを取得したら、データの各値は、挿入したデータのものと同じか
* データベースにデータを1件挿入して、nameでデータを取得して、各値を書き換えて、データ更新して、nameでデータ取得すると、データは更新されているか
* データベースにデータを1件挿入して、nameで削除操作をすると、データベースからデータが削除されるか

9.2 Adapterのテスト

Adapterのテストは、データベースを使わないので、ORMLiteを使ったアドレス帳アプリをテストしやすいように書き換え、Espressoでテストしたと同様にテストコードを書きます。

9.3 AsyncTaskLoaderのテスト

AsyncTaskLoaderのテストコードは次のとおりです。

PersonLoaderTest

public class PersonLoaderTest extends InjectedInstrumentationTest {

    private Context mContext;
    private PersonLoader mPersonLoader;


    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mContext = getInstrumentation().getTargetContext();
        mPersonLoader = new PersonLoader(mContext, mockDatabaseName);
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        mPersonLoader = null;
    }

    public void testSuccess() throws Exception {
        createTestData(200);

        List<Person> actual = mPersonLoader.getPersons();

        for (int i =0; i < actual.size(); i++) {
            Person actualPerson = actual.get(i);
            String actuallName = actualPerson.getName();
            Address actualAddress = actualPerson.getAddress();
            String actualZipcode = actualAddress.getZipcode();
            String actualPrefecture = actualAddress.getPrefecture();
            String acutalCity = actualAddress.getCity();
            String actualOther = actualAddress.getOther();

            assertThat(actualZipcode, is("123-456" + i));
            assertThat(actualPrefecture, is(("Tokyo")));
            assertThat(acutalCity, is(("Shinjyuku-ku")));
            assertThat(actualOther, is("Higashi-shinjyuku 1-2-" + i));
            assertThat(actuallName, is("Hoge" + i));
        }
    }

    public void testEmptyData() throws Exception {
        cleanData();

        List<Person> actual = mPersonLoader.getPersons();
        assertTrue(actual.isEmpty());
    }
}

今回は、AsyncTaskLoaderは、データベースからデータを読み込む処理に使い、テスト用データベースファイルを使いますので、テストクラスは、InjectedInstrumentationTestクラスを継承します。データベースのテストの時と同様に、

        mContext = getInstrumentation().getTargetContext();
        mPersonLoader = new PersonLoader(mContext, mockDatabaseName);

とすることにより、PersonLoaderコンストラクタの第二引数には、テスト用データベースファイルのファイル名を渡しています。

9.4 UIのテスト

UIのテストコードは次のとおりです。

public class MainActivityTest extends InjectedBaseActivityTest {
    private final static String TAG = MainActivityTest.class.getSimpleName();

    public MainActivityTest() {
       super();
    }

    public void testTransitionRegisterActivity1() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        assertFalse(activity.isFinishing());
        final ListView listView = (ListView) activity.findViewById(android.R.id.list);

        // The test works OK on API Level 21 emulator, but fails on API Level 19 emulator.
        assertEquals(200, listView.getCount());

        onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(0)
                .check(ViewAssertions.matches(ViewMatchers.isDisplayed()));

        onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(0)
                .perform(click());

        onView(withId(R.id.nameEt)).check(matches(withText("Hoge0")));
        onView(withId(R.id.zipcodeEt)).check(matches(withText("123-4560")));
        onView(withId(R.id.prefectureEt)).check(matches(withText("Tokyo")));
        onView(withId(R.id.cityEt)).check(matches(withText("Shinjyuku-ku")));
        onView(withId(R.id.otherEt)).check(matches(withText("Higashi-shinjyuku 1-2-0")));
        // todo : ボタンがUPDATEと表示されてることをテストしたい

        onView(withId(R.id.btn)).check(matches(isClickable()));
    }


    public void testTransitionRegisterActivity2() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        assertFalse(activity.isFinishing());

        onView(withId(R.id.input_add)).perform(click());

        onView(withId(R.id.nameEt)).check(matches(withText("")));
        onView(withId(R.id.zipcodeEt)).check(matches(withText("")));
        onView(withId(R.id.prefectureEt)).check(matches(withText("")));
        onView(withId(R.id.cityEt)).check(matches(withText("")));
        onView(withId(R.id.otherEt)).check(matches(withText("")));
        // todo : ボタンがREGISTERと表示されてることをテストしたい

        onView(withId(R.id.btn)).check(matches(isClickable()));
    }

    public void testAddPerson() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        onView(withId(R.id.input_add)).perform(click());

        assertFalse(activity.isFinishing());
        onView(withId(R.id.nameEt)).perform(typeText("Bar"));
        onView(withId(R.id.zipcodeEt)).perform(typeText("123-4567"));
        onView(withId(R.id.prefectureEt)).perform(typeText("Tokyo"));
        onView(withId(R.id.cityEt)).perform(typeText("Nerima-ku"));
        onView(withId(R.id.otherEt)).perform(typeText("Nerima 1-2-3"));

        onView(withId(R.id.btn)).perform(click());
    }

    public void testUpdateAddress() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        assertFalse(activity.isFinishing());

        onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(0)
                .perform(click());

        onView(withId(R.id.zipcodeEt)).perform(clearText()).perform(typeText("345-6789"));
        onView(withId(R.id.prefectureEt)).perform(clearText()).perform(typeText("Kyoto"));
        onView(withId(R.id.cityEt)).perform(clearText()).perform(typeText("Shimogyo-ku"));
        onView(withId(R.id.otherEt)).perform(clearText()).perform(typeText("Higashi 1-2-3"));
        onView(withId(R.id.btn)).perform(click());
        // todo: MainActivityに戻ったことをテストしたい
    }

    public void testNameEmptyInfo() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        assertFalse(activity.isFinishing());

        onView(withId(R.id.input_add)).perform(click());
        onView(withId(R.id.nameEt)).perform(typeText(""));

        // todo: うまくいかない
//        onView(withText("name is empty.")).inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
//                .check(matches(isDisplayed()));

        onView(withText("name is empty.")).inRoot(isToast());
    }

    public void testZipcodeEmptyInfo() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        assertFalse(activity.isFinishing());

        onView(withId(R.id.input_add)).perform(click());
        onView(withId(R.id.zipcodeEt)).perform(typeText(""));

        onView(withText("zipcode is empty.")).inRoot(isToast());
    }

    public void testPrefectureEmptyInfo() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        assertFalse(activity.isFinishing());

        onView(withId(R.id.input_add)).perform(click());
        onView(withId(R.id.prefectureEt)).perform(typeText(""));

        onView(withText("prefecture is empty.")).inRoot(isToast());
    }

    public void testCityEmptyInfo() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        assertFalse(activity.isFinishing());

        onView(withId(R.id.input_add)).perform(click());
        onView(withId(R.id.cityEt)).perform(typeText(""));

        onView(withText("city is empty.")).inRoot(isToast());
    }

    public void testOtherEmptyInfo() throws Throwable {
        // Creating dummy data
        createTestData(200);

        MainActivity activity = getActivity();

        assertFalse(activity.isFinishing());

        onView(withId(R.id.input_add)).perform(click());
        onView(withId(R.id.otherEt)).perform(typeText(""));

        onView(withText("other is empty.")).inRoot(isToast());
    }

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

このクラスは、上で定義したInjectedBaseActivityTestクラスを継承していて、InjectedBaseActivityTestでは、mockDatabaseNameに依存性注入を行っているので、このテストクラスは、テスト用データベースファイルを使用しています。

テスト項目は次のとおりです。

* Personリストが正常に表示されるか
* Person追加ボタンを押すと、登録画面に遷移するか
* Person追加ボタンを押して遷移した先の登録画面で、入力してボタンを正常に押せるか
* Personリストで、行をタップして、遷移した先の更新画面で、入力欄を書きかえて、更新ボタンを押せるか
* Person追加ボタンを押して遷移した先の登録画面で、入力欄に空があると、エラーのToastが表示されるか
* Personリストで、行をロングタップしたら、その行が削除されるか

10. ソースファイル公開

今回作成したアプリのソースファイルを公開します。

andropenguin/AddressBookORMLite12

11. 参考サイト

開発を効率的に進めるられるまでの道程
cattaka/FastCheckList
Android の非同期処理を行う Loader の起動方法
EspressoでToast表示のチェックをする
Instrumentation Testing with Dagger, Mockito, and Espresso
Dagger 2: Even sharper, less square
Dependency Injection With Dagger 2 on Android
gk5885/dagger-android-sample
Dagger 2 + Espresso 2 + Mockito

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です