ORMLiteを使ったアドレス帳アプリをテストしやすいように書き換え、Espressoでテストした

1. はじめに

以前公開したORMLiteを使ったアドレス帳アプリandropenguin/AddressBookORMliteをテストしやすいように書き直し、Espressoでテストを行います。

住友孝郎@cattaka_net氏のスライド開発を効率的に進めるられるまでの道程を読んで、データベース、Adapterがあるアプリで、どうテストしやすく書き換えるか、どうテストするか学びました。

上記スライドによると、データベース、Adapter、プリファレンスの単体テストを行い、また、Activityのテスト、つまりUIのテストは、データベース、Adapter、プリファレンスをダミーに置き換えてテストを行うと、いいそうです。上記スライドは、テストのやり方の概要を紹介するもので、実際のテストの仕方は、cattaka/FastCheckListのサンプルを読んで学びました。

以前公開したアドレス帳アプリAddressBookORMliteをテストしやすいように、adapter、app、asynctask、core、db、entityのサブパッケージを作ります。以前のアプリでは、adapterやdbに関するコードは、アクティビティに書いていましたし、また、DBから全件取得するコードを書いていたにも関わらず、AsyncTaskLoaderを使用していなかったので、asynctaskのサブパッケージも導入しました。あと、coreサブパッケージにあるクラスは、住友孝郎@cattaka_net氏が導入しているものです。

2. プロダクションコード

プロダクションコードの各クラスを見ていきます。

2.1 entityサブパッケージ

PersonクラスとAddressクラスは以下の通りです。多分内容は以前と変わっていないと思います。

Person.java

package com.sarltokyo.addressbookormlite7.entity;

import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "person")
public class Person {

    @DatabaseField(generatedId = true)
    private Integer id;
    @DatabaseField(unique = true)
    private String name;
    @DatabaseField(foreign = true, foreignAutoCreate = true, foreignAutoRefresh = true)
    private Address address;

    public Person() {
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}

Address.java

package com.sarltokyo.addressbookormlite7.entity;

import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;

@DatabaseTable(tableName = "address")
public class Address {

    @DatabaseField(generatedId = true)
    private Integer id;
    @DatabaseField
    private String zipcode;
    @DatabaseField
    private String prefecture;
    @DatabaseField
    private String city;
    @DatabaseField
    private String other;


    public Address() {
    }

    public Integer getId() {
        return id;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }

    public String getPrefecture() {
        return prefecture;
    }

    public void setPrefecture(String prefecture) {
        this.prefecture = prefecture;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getOther() {
        return other;
    }

    public void setOther(String other) {
        this.other = other;
    }
}

2.2 dbサブパッケージ

データベースに関するコードは次の通りです。以前は、CRUDのコードを他のクラスに書いていましたが、テストしやすいように、OpenHelperクラスに書きました。

OpenHelper.java

package com.sarltokyo.addressbookormlite7.db;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import com.sarltokyo.addressbookormlite7.entity.Address;
import com.sarltokyo.addressbookormlite7.entity.Person;

import java.sql.SQLException;
import java.util.List;

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

    private final static String DATABASE_NAME = "addressbook.db";
    private final static int DATABASE_VERSION = 1;

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

    public OpenHelper(Context context) {
        this(context, DATABASE_NAME);
    }


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

    @Override
    public void onCreate(SQLiteDatabase database, ConnectionSource connectionSource) {
        try {
            // エンティティを指定してcreate tableする
            TableUtils.createTable(connectionSource, Person.class);
            TableUtils.createTable(connectionSource, Address.class);
        } catch (SQLException e) {
            Log.e(TAG, "cannot create database");
            Log.e(TAG, e.getMessage());
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) {
        try {
            TableUtils.dropTable(connectionSource, Address.class, true);
            TableUtils.dropTable(connectionSource, Person.class, true);
            onCreate(database);
        } catch (SQLException e) {
            Log.e(TAG, "cannot upgrade database");
            Log.e(TAG, e.getMessage());
        }
    }

    public Dao<Person, Integer> getPersonDao() throws SQLException {
        if (mPersonDao == null) {
            mPersonDao = getDao(Person.class);
        }
        return mPersonDao;
    }

    public Dao<Address, Integer> getAddressDao() throws SQLException {
        if (mAddressDao == null) {
            mAddressDao = getDao(Address.class);
        }
        return mAddressDao;
    }

    public Person findPerson(String name) throws SQLException {
        List<Person> persons = getPersonDao().queryForEq("name", name);
        if (persons.isEmpty()) {
            Log.d(TAG, "persons isEmpty");
            return null;
        } else {
            Log.d(TAG, "person exists");
            return persons.get(0);
        }
    }

    public List<Person> findPerson() throws SQLException {
        return getPersonDao().queryForAll();
    }

    public void registerPerson(Person person) throws SQLException {
        getPersonDao().create(person);
    }

    public void updatePerson(Person person) throws SQLException {
        Address address = person.getAddress();
        getAddressDao().update(address); // todo
        getPersonDao().update(person);
    }

    public boolean deletePerson(String name) throws SQLException {
        Person person = findPerson(name);
        if (person == null) {
            return false;
        } else {
            Address address = person.getAddress(); // todo
            getAddressDao().delete(address);
            getPersonDao().delete(person);
            return true;
        }
    }
}

2.3 coreサブパッケージ

住友孝郎@cattaka_net氏のコードを使用しています。

2.4 asynctaskサブパッケージ

AsyncTaskLoaderはほぼ定番コードを使用するので、Android の非同期処理を行う Loader の起動方法で入手した、absractなAsyncTaskLoaderを使用します。下記コードで、使用ケースに応じて、Tにクラス名を書いて使用します。

AbstractAsyncTaskLoader.java

package com.sarltokyo.addressbookormlite7.asynctask;

import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;

/**
 * Android の非同期処理を行う Loader の起動方法
 * http://tomoyamkung.net/2014/02/24/android-loader-execute/
 */
public abstract class AbstractAsyncTaskLoader<T> extends AsyncTaskLoader<T> {

    protected T mResult;

    public AbstractAsyncTaskLoader(Context context) {
        super(context);
    }


    abstract public T loadInBackground();

    @Override
    public void deliverResult(T data) {
        if (isReset()) {
            return;
        }

        mResult = data;
        if (isStarted()) {
            super.deliverResult(data);
        }
    }

    @Override
    protected void onStartLoading() {
        if (mResult != null) {
            deliverResult(mResult);
        }

        if (takeContentChanged() || mResult == null) {
            forceLoad(); // 非同期処理を開始
        }
    }

    @Override
    protected void onStopLoading() {
        cancelLoad(); // 非同期処理のキャンセル
    }

    @Override
    public void onCanceled(T data) {
        // nop
    }

    @Override
    protected void onReset() {
        super.onReset();

        onStopLoading();
        mResult = null;
    }
}

実際に使用するPersonLoader.javaは以下の通りです。上記AbstractAsyncTaskLoaderクラスで、TをListにしたクラスを継承します。AsyncTaskLoaderクラスについては、十分テストが行われていると思われるので、loadInBackground内の処理のみをテストするため、loadInBackground内の処理をメソッド抽出しています。

PersonLoader.java

import java.util.List;

public class PersonLoader extends AbstractAsyncTaskLoader<List<Person>> {

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

    private ContextLogic mContextLogic;
    private OpenHelper mOpenHelper;

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

    @Override
    public List<Person> loadInBackground() {
        return getPersons();
    }

    public List<Person> getPersons() {
        List<Person> persons = null;
        try {
            persons = mOpenHelper.findPerson();
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
        }
        if (persons == null) {
            persons = new ArrayList<Person>();
        }

        if (persons.size() == 0) {
            Log.d(TAG, "person is empty");
        }
        for (Person person: persons) {
            Log.d(TAG, "person = " + person.getName());
        }
        return persons;
    }
}

2.5 appサブパッケージ

アクティビティを置いたサブパッケージです。

MainActivity.java

package com.sarltokyo.addressbookormlite7.app;

import android.app.ProgressDialog;
import android.content.Intent;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;
import com.sarltokyo.addressbookormlite7.adapter.AdapterEx;
import com.sarltokyo.addressbookormlite7.asynctask.PersonLoader;
import com.sarltokyo.addressbookormlite7.core.ContextLogic;
import com.sarltokyo.addressbookormlite7.core.ContextLogicFactory;
import com.sarltokyo.addressbookormlite7.db.OpenHelper;
import com.sarltokyo.addressbookormlite7.entity.Person;

import java.sql.SQLException;
import java.util.List;


public class MainActivity extends AppCompatActivity
        implements LoaderManager.LoaderCallbacks<List<Person>> {

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

    private ListView mListView;
    private OpenHelper mOpenHelper;

    public final static String TYPE = "type";
    public final static String CREATE_DATA_TYPE = "create";
    public final static String UPDATE_DATA_TYPE = "update";

    private AdapterEx mPersonsAdapter;
    private ContextLogic mContextLogic;
    private ProgressDialog mProgressDialog;


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

        // todo: ここ重要。DBをproduction用、test用に切り替えられるようにする。
        mContextLogic = ContextLogicFactory.createContextLogic(this);
        mOpenHelper = mContextLogic.createOpenHelper();
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume called");
        showProgress();
        LoaderManager manager = getSupportLoaderManager();
        if (manager.getLoader(0) != null) {
            manager.destroyLoader(0);
        }
        manager.initLoader(0, null, this);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        } else if (id == R.id.input_add) {
            Intent intent = new Intent(MainActivity.this, RegisterActivity.class);
            intent.putExtra(TYPE, CREATE_DATA_TYPE);
            startActivity(intent);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void showProgress() {
        Log.d(TAG, "showProcess()");
        mProgressDialog = new ProgressDialog(this);
        mProgressDialog.setMessage("running");
        mProgressDialog.show();
    }

    private void dismissProgress() {
        Log.d(TAG, "dismissProgress()");
        mProgressDialog.dismiss();
    }

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

    @Override
    public void onLoadFinished(Loader<List<Person>> loader, List<Person> data) {
        Log.d(TAG, "PersonLoader finished");
        dismissProgress();

        if (data != null) {
            mPersonsAdapter = new AdapterEx(this, data);
            mListView = (ListView)findViewById(android.R.id.list);
            mListView.setAdapter(mPersonsAdapter);

            mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    updateAddress(view);
                }
            });

            mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
                @Override
                public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                    return deletePerson(view);
                }
            });
        } else {
            // todo
            Toast.makeText(this, "Some error occured.", Toast.LENGTH_LONG).show();;
        }

        LoaderManager manager = getSupportLoaderManager();
        // 既にローダーがある場合は破棄
        if (manager.getLoader(0) != null) {
            manager.destroyLoader(0);
        }
    }

    @Override
    public void onLoaderReset(Loader<List<Person>> loader) {
        // nop
    }

    private void updateAddress(View view) {
        Log.d(TAG, "updateAddress is called");

        int positon;
        String name = null;

        if (!(view.getTag() instanceof Integer)) {
            return;
        }
        positon = (Integer)view.getTag();
        if (positon < 0 | mPersonsAdapter.getCount() <= positon) {
            return;
        }

        Log.d(TAG, "position in updateAddress = " + positon);
        Person person = mPersonsAdapter.getItem(positon);
        Log.d(TAG, "person in updateAddress = " + person);
        try {
            name = mOpenHelper.findPerson(person.getName()).getName();
            Log.d(TAG, "name in updateAddress = " + name);
        } catch (SQLException e) {
            Log.e(TAG, e.getMessage());
        }
        if (name == null) return; // todo: 多分、ありえない
        Intent intent = new Intent(MainActivity.this, RegisterActivity.class);
        intent.putExtra(TYPE, UPDATE_DATA_TYPE);
        intent.putExtra("name", name);
        startActivity(intent);
    }

    private boolean deletePerson(View view) {
        int positon;

        if (!(view.getTag() instanceof Integer)) {
            return true;
        }
        positon = (Integer)view.getTag();
        if (positon < 0 | mPersonsAdapter.getCount() <= positon) {
            return true;
        }

        Person person = mPersonsAdapter.getItem(positon);
        try {
            boolean isDeleted = mOpenHelper.deletePerson(person.getName());
            if (isDeleted) {
                Toast.makeText(MainActivity.this, person.getName() + " was deleted.", Toast.LENGTH_LONG).show();

                List<Person> list = mOpenHelper.findPerson();
                mPersonsAdapter = new AdapterEx(this, list);
                mListView.setAdapter(mPersonsAdapter);
                mPersonsAdapter.notifyDataSetChanged();
            } else {
                Toast.makeText(MainActivity.this, person.getName() + " cannot be deleted.", Toast.LENGTH_LONG).show();
            }
            return true;
        } catch (SQLException e) {
            Toast.makeText(MainActivity.this, person.getName() + " cannot be deleted.", Toast.LENGTH_LONG).show();
            Log.e(TAG, e.getMessage());
            return true;
        }
    }

}

person追加かアドレス更新の時に遷移する先のアクティビティ
RegisterActivity.java

package com.sarltokyo.addressbookormlite7.app;

import android.content.res.Resources;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.sarltokyo.addressbookormlite7.core.ContextLogic;
import com.sarltokyo.addressbookormlite7.core.ContextLogicFactory;
import com.sarltokyo.addressbookormlite7.db.OpenHelper;
import com.sarltokyo.addressbookormlite7.entity.Address;
import com.sarltokyo.addressbookormlite7.entity.Person;

import java.sql.SQLException;

/**
 * Created by osabe on 15/07/17.
 */
public class RegisterActivity extends AppCompatActivity
        implements View.OnClickListener {
    private final static String TAG = RegisterActivity.class.getSimpleName();

    private ContextLogic mContextLogic;
    private OpenHelper mOpenHelper;
    private Button mBtn;

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

        // todo: ここ重要。DBをproduction用、test用に切り替えられるようにする。
        mContextLogic = ContextLogicFactory.createContextLogic(this);
        mOpenHelper = mContextLogic.createOpenHelper();

        mBtn = (Button)findViewById(R.id.btn);
        mBtn.setOnClickListener(this);

        String type = getIntent().getStringExtra(MainActivity.TYPE);
        String name = getIntent().getStringExtra("name");

        Resources resources = getResources();
        if (type.equals(MainActivity.CREATE_DATA_TYPE)) {
            mBtn.setText(resources.getString(R.string.register));
        } else if (type.equals(MainActivity.UPDATE_DATA_TYPE)) {
            mBtn.setText(resources.getString(R.string.update));
            showAddress(name);
        }
    }

    @Override
    public void onClick(View view) {
        int id = view.getId();
        if (id == R.id.btn) {
            String type = getIntent().getStringExtra(MainActivity.TYPE);
            if (type.equals(MainActivity.CREATE_DATA_TYPE)) {
                registerAddressBook();
            } else if (type.equals(MainActivity.UPDATE_DATA_TYPE)) {
                String name = getIntent().getStringExtra("name");
                updateAddressBook(name);
            }
        }
    }

    public void registerAddressBook() {
        String name = ((EditText)findViewById(R.id.nameEt)).getText().toString();
        String zipcode = ((EditText)findViewById(R.id.zipcodeEt)).getText().toString();
        String prefecture = ((EditText)findViewById(R.id.prefectureEt)).getText().toString();
        String city = ((EditText)findViewById(R.id.cityEt)).getText().toString();
        String other = ((EditText)findViewById(R.id.otherEt)).getText().toString();

        if (!checkData(name, zipcode, prefecture, city, other)) {
            return;
        }

        Address address = new Address();
        address.setZipcode(zipcode);
        address.setPrefecture(prefecture);
        address.setCity(city);
        address.setOther(other);
        Person person = new Person();
        person.setName(name);
        person.setAddress(address);
        try {
            mOpenHelper.registerPerson(person);
            finish();
        } catch (SQLException e) {
            Toast.makeText(this, "cannot register address.", Toast.LENGTH_LONG).show();
            Log.e(TAG, e.getMessage());
        }
    }

    public void updateAddressBook(String name) {
        String newName = ((EditText)findViewById(R.id.nameEt)).getText().toString();
        String zipcode = ((EditText)findViewById(R.id.zipcodeEt)).getText().toString();
        String prefecture = ((EditText)findViewById(R.id.prefectureEt)).getText().toString();
        String city = ((EditText)findViewById(R.id.cityEt)).getText().toString();
        String other = ((EditText)findViewById(R.id.otherEt)).getText().toString();

        if (!checkData(name, zipcode, prefecture, city, other)) {
            return;
        }

        try {
            Person person = mOpenHelper.findPerson(name);
            Address address = person.getAddress();
            address.setZipcode(zipcode);
            address.setPrefecture(prefecture);
            address.setCity(city);
            address.setOther(other);
            person.setName(newName);
            person.setAddress(address);
            mOpenHelper.updatePerson(person);
            finish();
        } catch (SQLException e) {
            Toast.makeText(this, "cannot update address.", Toast.LENGTH_LONG).show();
            Log.e(TAG, e.getMessage());
        }
    }

    public void showAddress(String name) {
        String type = getIntent().getStringExtra(MainActivity.TYPE);
        if (!type.equals(MainActivity.UPDATE_DATA_TYPE)) return;

        Person person;
        Address address;

        try {
            person = mOpenHelper.findPerson(name);

        } catch (SQLException e) {
            // ありえない
            Log.e(TAG, e.getMessage());
            return;
        }

        address = person.getAddress();

        String zipcode = address.getZipcode();
        String prefecture = address.getPrefecture();
        String city = address.getCity();
        String other = address.getOther();

        ((EditText)findViewById(R.id.nameEt)).setText(name);
        ((EditText)findViewById(R.id.zipcodeEt)).setText(zipcode);
        ((EditText)findViewById(R.id.prefectureEt)).setText(prefecture);
        ((EditText)findViewById(R.id.cityEt)).setText(city);
        ((EditText)findViewById(R.id.otherEt)).setText(other);
    }

    public boolean checkData(String name, String zipcode, String prefecture,
                             String city, String other) {
        if (TextUtils.isEmpty(name)) {
            Toast.makeText(this, "name is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(zipcode)) {
            Toast.makeText(this, "zipcode is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(prefecture)) {
            Toast.makeText(this, "prefecture is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(city)) {
            Toast.makeText(this, "city is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        if (TextUtils.isEmpty(other)) {
            Toast.makeText(this, "other is empty.", Toast.LENGTH_LONG).show();
            return false;
        }
        return true;
    }
}

ここで、coreサプパッケージのContexLogic、ContexLogicFactoryクラスを使って、アクティビティクラスのonCreateメソッド内で、

 // todo: ここ重要。DBをproduction用、test用に切り替えられるようにする。
        mContextLogic = ContextLogicFactory.createContextLogic(this);
        mOpenHelper = mContextLogic.createOpenHelper();

のようにコーディングすると、アプリ使用時には実際のデータベースファイルaddressbook.db、テスト時には、test_addressbook.db ファイルを使用できるようになります。ContexLogic、ContexLogicFactoryクラスクラスについては、cattaka/FastCheckListのサンプルにファイルがあります。

2.6 adapterサブパッケージ

アダプタのコードは、以前はMainActivity.javaに書いていましたが、テストしやすいように、独立なクラスにします。

AdapterEx.java

package com.sarltokyo.addressbookormlite7.adapter;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import com.sarltokyo.addressbookormlite7.app.R;
import com.sarltokyo.addressbookormlite7.entity.Person;

import java.util.List;

public class AdapterEx extends ArrayAdapter<Person> {
    public AdapterEx(Context context, List<Person> persons) {
        super(context, R.layout.layout_main_item, persons);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Person person = getItem(position);
        if (convertView == null) {
            LayoutInflater inflater = LayoutInflater.from(getContext());
            convertView = inflater.inflate(R.layout.layout_main_item, null);
        }
        convertView.setTag(position);
        convertView.findViewById(R.id.nameTv).setTag(position);
        TextView nameTv = (TextView)convertView.findViewById(R.id.nameTv);
        nameTv.setText(person.getName());
        return convertView;
    }
}

3. Espressoを使ったテストの準備

Espress 2.2を使うと、テストランナー android.support.test.runner.AndroidJUnitRunner で、データベース、アダプター、プリファレンス、UIのテストをすべて行えます。

app/build.gradleを以下のように編集します。applicationIdやtestApplicationIdは適宜修正します。なお、不要なライブラリを参照しているかもしれません。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.1.1'
    }
}
apply plugin: 'com.android.application'

repositories {
    jcenter()
}

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "com.sarltokyo.addressbookormlite7.app"
        minSdkVersion 9
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"

        testApplicationId "com.sarltokyo.addressbookormlite7.app.test"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_6
        targetCompatibility JavaVersion.VERSION_1_6
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            testCoverageEnabled true
        }
    }
    packagingOptions {
        exclude 'LICENSE.txt'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.j256.ormlite:ormlite-android:4.48'
    androidTestCompile ('com.android.support.test:runner:0.3') {
        exclude module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test:rules:0.3') {
        exclude module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test.espresso:espresso-core:2.2') {
        exclude module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test.espresso:espresso-idling-resource:2.2') {
        exclude module: 'support-annotations'
    }
    androidTestCompile 'org.mockito:mockito-core:1.10.19'
    androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
}

4. Espressoを使ったテスト

4.1 テスト用ツール類 app.testサブパッケージ

住友孝郎@cattaka_net氏のBaseTestCase、TestContextLogic、TestUtil、UnlockKeyguardActivityクラスを使用します。https://github.com/cattaka/FastCheckList/tree/master/app/src/androidTest/java/net/cattaka/android/fastchecklist/test からファイルは入手可能です。本記事では、データベースのクリア、ダミーデータの作成のために、BaseTestCaseを改変しました。

BaseTestCase.java

package com.sarltokyo.addressbookormlite7.app.test;

import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.os.SystemClock;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import com.sarltokyo.addressbookormlite7.core.ContextLogic;
import com.sarltokyo.addressbookormlite7.core.ContextLogicFactory;
import com.sarltokyo.addressbookormlite7.db.OpenHelper;
import com.sarltokyo.addressbookormlite7.entity.Address;
import com.sarltokyo.addressbookormlite7.entity.Person;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by osabe on 15/07/14.
 *
 * This file is a modified version of BaseTestCase.java
 * https://github.com/cattaka/FastCheckList/blob/master/app/src/androidTest/java/net/cattaka/android/fastchecklist/test/BaseTestCase.java
 */
public class BaseTestCase<T extends Activity> extends ActivityInstrumentationTestCase2<T> {
    protected ContextLogic mContextLogic;
    private final static String TAG = BaseTestCase.class.getSimpleName();

    public BaseTestCase(Class<T> tClass) {
        super(tClass);
    }

    protected void setUp() throws Exception {
        super.setUp();
        Context context = getInstrumentation().getTargetContext();
        mContextLogic = new TestContextLogic(context);
        {   // Replace ContextLogicFactory to use RenamingDelegatingContext.
            ContextLogicFactory.replaceInstance(new ContextLogicFactory() {
                @Override
                public ContextLogic newInstance(Context context) {
                    return mContextLogic;
                }
            });
        }
        {   // Unlock keyguard and screen on
            KeyguardManager km = (KeyguardManager) getInstrumentation()
                    .getTargetContext().getSystemService(Context.KEYGUARD_SERVICE);
            PowerManager pm = (PowerManager) getInstrumentation()
                    .getTargetContext().getSystemService(Context.POWER_SERVICE);
            if (km.inKeyguardRestrictedInputMode() || !pm.isScreenOn()) {
                Intent intent = new Intent(getInstrumentation().getContext(), UnlockKeyguardActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                getInstrumentation().getTargetContext().startActivity(intent);
                while (km.inKeyguardRestrictedInputMode()) {
                    SystemClock.sleep(100);
                }
            }
        }

        cleanData();
    }

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

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

        for (Person person : persons) {
            try {
                openHelper.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.
        OpenHelper openHelper = mContextLogic.createOpenHelper();
        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("Higash-shinjyuku 1-2-" + i);
            person.setName("Hoge" + i);
            person.setAddress(address);
            try {
                openHelper.registerPerson(person);
                persons.add(person);
            } catch (SQLException e) {
                Log.e(TAG, e.getMessage());
            }
        }
        return persons;
    }
}
&#91;/java&#93;

4.2 アダプターのテスト

アダプターのテストは、AdapterExのコンストラクタがAdapterEx(Context context, List<Person> persons)のようにList<Person>型の引数を持ちますので、そこにダミーのデータを入れて、Viewが期待した通りになるかテストします。


adapterサブパッケージ
AdapterExTest.java

package com.sarltokyo.addressbookormlite7.adapter;

import android.content.Context;
import android.test.InstrumentationTestCase;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.sarltokyo.addressbookormlite7.app.R;
import com.sarltokyo.addressbookormlite7.entity.Address;
import com.sarltokyo.addressbookormlite7.entity.Person;
import org.hamcrest.Matchers;

import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

public class AdapterExTest extends InstrumentationTestCase {
    public void testGetView() {
        List<Person> dummys = new ArrayList<Person>();
        Person person1 = new Person();
        Address address1 = new Address();
        address1.setZipcode("123-4567");
        address1.setPrefecture("Tokyo");
        address1.setCity("Shinjuku");
        address1.setOther("hoge 1-2-3");
        person1.setName("Foo");
        person1.setAddress(address1);

        Person person2 = new Person();
        Address address2 = new Address();
        address2.setZipcode("111-2222");
        address2.setPrefecture("Kyoto");
        address2.setCity("Kyoto");
        address2.setOther("boo 4-5-6");
        person2.setName("Bar");
        person2.setAddress(address2);

        dummys.add(person1);
        dummys.add(person2);

        Context context = getInstrumentation().getTargetContext();
        AdapterEx sut = new AdapterEx(context, dummys);

        View view1 = sut.getView(0, null, null);
        assertThat(view1, is(Matchers.instanceOf(LinearLayout.class)));
        assertThat(view1.findViewById(R.id.nameTv), is(Matchers.instanceOf(TextView.class)));
        assertThat(((TextView)view1.findViewById(R.id.nameTv)).getText().toString(), is("Foo"));

        View view2 = sut.getView(1, null, null);
        assertThat(view2, is(Matchers.instanceOf(LinearLayout.class)));
        assertThat(view2.findViewById(R.id.nameTv), is(Matchers.instanceOf(TextView.class)));
        assertThat(((TextView)view2.findViewById(R.id.nameTv)).getText().toString(), is("Bar"));
    }
}

4.3 AsyncTaskLoaderのテスト

PersonLoaderのテストは、メソッド抽出したgetPersonsメソッドが、ダミーなデータ入力に対して、期待した値を返すかでテストします。

asynctaskサブパッケージ

PersonLoaderTest.java

package com.sarltokyo.addressbookormlite7.asynctask;

import android.content.Context;
import android.test.InstrumentationTestCase;
import android.test.RenamingDelegatingContext;
import com.sarltokyo.addressbookormlite7.db.OpenHelper;
import com.sarltokyo.addressbookormlite7.entity.Address;
import com.sarltokyo.addressbookormlite7.entity.Person;

import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

public class PersonLoaderTest extends InstrumentationTestCase {

    private OpenHelper mOpenHelper;
    private Context mContext;
    private PersonLoader mPersonLoader;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mContext = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_" );
        mOpenHelper = new OpenHelper(mContext);
        mPersonLoader = new PersonLoader(mContext);

    }

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

    private void createRegularData() throws Exception {
        // insert
        for (int i = 0; i < 10; i++) {
            Person person = new Person();
            Address address = new Address();
            address.setZipcode(String.valueOf(i));
            address.setPrefecture("Tokyo");
            address.setCity("Shinjyuku-ku");
            address.setOther("Higashi-shinjyuku " + (i+1));
            person.setName("Abe" + i);
            person.setAddress(address);
            mOpenHelper.registerPerson(person);
        }
    }

    private void clearData() throws Exception {
        List<Person> persons = mOpenHelper.findPerson();
        for (Person person: persons) {
            mOpenHelper.deletePerson(person.getName());
        }
    }


    public void testSucces() throws Exception {
        createRegularData();

        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(String.valueOf(i)));
            assertThat(actualPrefecture, is(("Tokyo")));
            assertThat(acutalCity, is(("Shinjyuku-ku")));
            assertThat(actualOther, is("Higashi-shinjyuku " + (i+1)));
            assertThat(actuallName, is("Abe" + i));
        }
    }

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

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


    }
}

データベースをダミーのものに置き換えてテストを行うには、

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        Context context = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_" );
        mOpenHelper = new OpenHelper(context);
    }

のように、contextを、 new RenamingDelegatingContext(getInstrumentation().getTargetContext(), “test_” );で置き換えることが重要です。これを行うと、データベースファイルは、実際のデータベースファイル名の頭に”test_”がついて、ダミーのデータベースファイルでテストを行うことができます。

4.4 coreサブパッケージのクラスのテスト

FastCheckList/app/src/androidTest/java/net/cattaka/android/fastchecklist/core/で、パッケージを修正して、テストします。

4.5 データベースのテスト

データベースのテストはダミーのデータファイルを使用してテストを行います。

dbサブパッケージ
OpenHelperTest.java

package com.sarltokyo.addressbookormlite7.db;

import android.content.Context;
import android.test.InstrumentationTestCase;
import android.test.RenamingDelegatingContext;
import com.sarltokyo.addressbookormlite7.entity.Address;
import com.sarltokyo.addressbookormlite7.entity.Person;

import java.sql.SQLException;
import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

public class OpenHelperTest extends InstrumentationTestCase {
    private OpenHelper mOpenHelper;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        Context context = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_" );
        mOpenHelper = new OpenHelper(context);
    }

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

    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
        for (int i = 0; i < 10; i++) {
            Person person = new Person();
            Address address = new Address();
            address.setZipcode(String.valueOf(i));
            address.setPrefecture("Tokyo");
            address.setCity("Shinjyuku-ku");
            address.setOther("Higashi-shinjyuku " + (i+1));
            person.setName("Abe" + i);
            person.setAddress(address);
            mOpenHelper.registerPerson(person);
        }
        // select
        List<Person> sut = mOpenHelper.findPerson();
        assertEquals(10, 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, mOpenHelper.findPerson().size());
    }

    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, mOpenHelper.findPerson().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());
    }
}

データベースのテストをダミーデータで行うためには、

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        Context context = new RenamingDelegatingContext(getInstrumentation().getTargetContext(), "test_" );
        mOpenHelper = new OpenHelper(context);
    }

のように、contextを、 new RenamingDelegatingContext(getInstrumentation().getTargetContext(), “test_” );で置き換えることが重要です。これを行うと、データベースファイルは、実際のデータベースファイル名の頭に”test_”がついて、ダミーのデータベースファイルでテストを行うことができます。

4.6 UIのテスト

アクティビティ、つまりUIのテストは、ダミーなデータベースファイルを使用してテストを行います。

MainActivity3Test.java

package com.sarltokyo.addressbookormlite7.app;

import android.os.IBinder;
import android.support.test.espresso.*;
import android.support.test.espresso.assertion.ViewAssertions;
import android.support.test.espresso.matcher.ViewMatchers;
import android.view.WindowManager;
import android.widget.ListView;
import com.sarltokyo.addressbookormlite7.app.test.BaseTestCase;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;


import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.*;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.*;
import static org.hamcrest.Matchers.anything;

public class MainActivity3Test extends BaseTestCase<MainActivity> {
    private final static String TAG = MainActivity3Test.class.getSimpleName();


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



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


        // todo: これでOK
        onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(0)
                .perform(click());
        // todo: これでOK
        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("Higash-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());
    }


//    private void setSelection(final ListView listView, final int position) throws Throwable {
//        runTestOnUiThread(new Runnable() {
//            @Override
//            public void run() {
//                listView.setSelection(position);
//            }
//        });
//    }
////    private void select(final int position) throws Throwable {
////        runTestOnUiThread(new Runnable() {
////            @Override
////            public void run() {
////                onData(allOf(hasToString(startsWith("Hoge")))).inAdapterView(withId(android.R.id.list)).atPosition(position)
////                        .perform(click());
////            }
////        });
////    }

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

このテストでは、テストクラスが、BaseTestCaseを継承しており、BaseTestTestCaseクラスのsetUpメソッド内で、

    protected void setUp() throws Exception {
        super.setUp();
        Context context = getInstrumentation().getTargetContext();
        mContextLogic = new TestContextLogic(context);
        {   // Replace ContextLogicFactory to use RenamingDelegatingContext.
            ContextLogicFactory.replaceInstance(new ContextLogicFactory() {
                @Override
                public ContextLogic newInstance(Context context) {
                    return mContextLogic;
                }
            });
        }
...
    }

と書かれています。TestContextLogicクラスは、

public class TestContextLogic extends ContextLogic {
    private RenamingDelegatingContext mRdContext;
    public TestContextLogic(Context context) {
        super(context);
        mRdContext = new RenamingDelegatingContext(context, "test_");
    }

    @Override
    public OpenHelper createOpenHelper() {
        return new OpenHelper(mRdContext);
    }
}

になっており、ContextLogicFactoryクラスは、

public class ContextLogicFactory {
    static ContextLogicFactory INSTANCE = new ContextLogicFactory();

    public ContextLogic newInstance(Context context) {
        return new ContextLogic(context);
    }

    public static ContextLogic createContextLogic(Context context) {
        return INSTANCE.newInstance(context);
    }

    public static void replaceInstance(ContextLogicFactory INSTANCE) {
        ContextLogicFactory.INSTANCE = INSTANCE;
    }
}

になっています。これらのコードで、テスト時に、実際のデータベースファイルのファイル名の頭に、”test_”をつけたファイル名のデータベースを使用するようになっています。

アクティビティのテストでは、ダミーデータを用意しておいた場合、

メイン画面で全件表示できるか
メイン画面のリストで行をタップすると、Address更新画面に遷移するか
遷移先でアドレスを更新できるか
+ボタンのタップで、名前、アドレス入力画面に遷移するか、名前、アドレスを入力できるか、personを追加できるか
アドレス入力画面で、入力欄が空欄の場合、Toast表示を確認できるか

などをテストしています。

5. ソースファイル公開

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

andropenguin/AddressBookORMLite7

6. 参考サイト

開発を効率的に進めるられるまでの道程
cattaka/FastCheckList
Android の非同期処理を行う Loader の起動方法
EspressoでToast表示のチェックをする

コメントを残す

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