連載
» 2012年02月24日 00時00分 公開

スマホアプリに必須なデータ永続化のためのDBテストAndroidアプリ開発テスト入門(4)(3/3 ページ)

[吉澤毅,日本Androidの会テスト部]
前のページへ 1|2|3       

【2】マイグレーションのテスト

 アプリをリリースした後、アプリのアップグレードとともにDBの変更が発生することがあります。AndroidのSQLiteOpenHelperは、アップグレード時にDBを更新できる仕組みがあるので(API Level 11からはダウングレードも提供)、使ってみましょう。

 キャッシュ用途でDBを利用している場合はアップグレード時にテーブルをdropして再作成し直すこともありますが、データを永続化しておきたい場合はデータを破壊せずにデータ移行を完了しなければいけません。

バージョンアップのパターンはユーザーで異なる

 Webサーバを使うWebアプリのマイグレーションと違い、スマホのネイティブアプリでは各ユーザーごとにマイグレーションが必要です。Webサーバの場合はシステム側の任意のタイミングで実行できますが、クライアントの場合は、いつ実行されるか分かりませんし、バージョンアップごとにユーザーがインストールしてアップデートされる保証もありません。「もしかしたら1年ぶりにアップデートするユーザーがいる」と考える必要があります。

 アプリの成長とともに、すべてのバージョンアップのパターンを網羅して手動でテストすることは現実的ではありません。マイグレーション時にデータを移行するための変換作業があるのであれば、それなりの変換パターンを検証する必要があります。

マイグレーションテストのコード例

 そこで、各バージョンからのマイグレーションもテストするようにし、安心してDBをアップグレードできるようにしましょう。

 ここでは、「作成日時のほかに、更新日時も格納する必要が生じ、これまで登録してあるデータに関しては更新日時を作成日時と同じにする」という仕様を想定します。内部のDBバージョンを1から2に上げて対応してみます。

public abstract class MigrationOpenHelper extends SQLiteOpenHelper {
 
    private static final String DB_NAME = "app";
 
    public MigrationOpenHelper(Context context, int version) {
        super(context, DB_NAME, null, version);
    }
 
    /**
     * すべてのマイグレーションコードをここに記述
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion < 2) {
            db.execSQL("alter table user add column updatedAt integer");
            db.execSQL("update user set updatedAt = createdAt");
        }
    }
}
 
public class MigrationOpenHelperV1 extends MigrationOpenHelper {
    public MigrationOpenHelperV1(Context context) {
        super(context, 1);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("create table user(_id integer primary key autoincrement, name text, createdAt integer)");
    }
}
 
public class MigrationOpenHelperV2 extends MigrationOpenHelper {
    public MigrationOpenHelperV2(Context context) {
        super(context, 2);
    }
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("create table user(_id integer primary key autoincrement, name text, createdAt integer, updatedAt integer)");
    }
}
 
/**
 * 最新バージョンの {@link SQLiteOpenHelper} クラスを生成するファクトリクラス
 */
public class MigrationOpenHelperFactory {
    protected MigrationOpenHelperFactory() {
    }
    public static SQLiteOpenHelper create(Context context) {
        return new MigrationOpenHelperV2(context);
    }
}
 
public class MigrationOpenHelperV1Test extends AndroidTestCase {
    private Context context;
    public void setUp() {
        context = new RenamingDelegatingContext(getContext(), "test_");
        SQLiteOpenHelper helperV1 = new MigrationOpenHelperV1(context);
        try {
            SQLiteDatabase db = helperV1.getWritableDatabase();
            try {
                // 初期データ
                db.execSQL("insert into user(name, createdAt) values ('宮田', 1326361400)");
                db.execSQL("insert into user(name, createdAt) values ('渡辺', 1326361400)");
                db.execSQL("insert into user(name, createdAt) values ('吉澤', 1326361500)");
                db.execSQL("insert into user(name, createdAt) values ('吉沢', 1326361600)");
            } finally {
                db.close();
            }
        } finally {
            helperV1.close();
        }
    }
    public void testV1toLatest() {
        SQLiteOpenHelper helper = MigrationOpenHelperFactory.create(context);
        try {
            SQLiteDatabase db = helper.getWritableDatabase();
 
            try {
                UserDao userDao = new SQLiteUserDao(db);
                List<User> users = userDao.findAll();
                assertEquals("件数が変わっていないこと", 4, users.size());
                assertEquals("ユーザーの順序が変わっていないこと", "宮田", users.get(0).getName());
                assertEquals("ユーザーの順序が変わっていないこと", "渡辺", users.get(1).getName());
                assertEquals("ユーザーの順序が変わっていないこと", "吉澤", users.get(2).getName());
                assertEquals("ユーザーの順序が変わっていないこと", "吉沢", users.get(3).getName());
                
                for (User user : users) {
                    assertEquals("更新日時が移行できていること", user.getCreatedAt(), user.getUpdatedAt());
                }
            } finally {
                db.close();
            }
        } finally {
            helper.close();
        }
    }
}

 ポイントは、親クラスのSQLiteOpenHelperのコンストラクタにバージョン番号を渡せるようにしておくことです。こうすることで、ユーザーが任意のDBバージョン、すなわち任意のアプリバージョンから最新のアプリをインストールすることをシミュレーションできます。

 また今回は、バージョンごとにSQLiteOpenHelperクラスを実装してみました。少し変わった作りになりましたが、すべてのバージョンに対応する新規テーブル作成SQL文を1つのSQLiteOpenHelperクラスには置ききれないと判断したためです。

 ここに関しては、まだ試行錯誤をしている段階で、ぜひ良い方法を皆さんも考えてみてください。

テストできるところはテストしておこう

 今回はDBへのテストの書き方について解説してきました。DBのテストをコードで記述してデータ処理のテストとマイグレーションのテストを個人環境に縛られず行えます。安心してデータを取り扱えて、バージョンアップも怖くなくなります。

 ビジネスロジック同様にテストが記述しやすい場所なので、ぜひ活用してみてください。

 今回の記事のサンプルコードは以下の場所にあります。

著者紹介

Androidテスト部 部員

吉澤毅(@ussy00

株式会社ヌーラボにて、受託案件および自社サービスのサポート開発を行うプログラマ



前のページへ 1|2|3       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。