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;
}
}
[/java]
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表示のチェックをする