2015/11/04

Dagger2でRealmConfigurationの切り替え

はじめに

Realmを使ったアプリで本番用とテスト用とで*.realmファイルを分ける方法を試した.

RenamingDelegatingContextを使ってファイル名にデバッグ用prefix(“test_”等)を付けるとか, テストではRealmインスタンスを直接操作する方法があるが, 今回は下記の理由からDI(Dagger2)で頑張る方法を採用した.

  1. Realmのmigrationテストやencryptテストにも備えて, RealmConfigurationレベルでカスタムしたい.
  2. Realmインスタンスを直接操作せず, できるだけ本番に近いアーキテクチャでテストしたい.

今回の例ではわかりやすく, *.realmファイル名を本番用とテスト用とで分けることを題材にしている.
ただし, 本番用プロダクトへの影響を懸念するならtestApplicationIdでユーザランドを分けてしまうのが無難.

ApplicationIdについて, Google APIやWeb Service系でパッケージ名を一種の認証識別情報としているものがあったり, アプリ間連携で呼出し元のパッケージ名が重要な意味を持つ設計思想もあったりで, パッケージ名変更するのも気を付けないといけないポイントがいくつかある.
とはいえ, ApplicationIdを分けないと本番用/デバッグ用を完全に分離するのが困難なケース(後述)もあるため, その点には注意したい.

今回はこちらのレポジトリからの抜粋となる.

Realm factory

Realmのインスタンス生成は専用のFactoryクラスRealmFactory内にカプセル化する.

public class RealmFactory {
  public Realm getRealm() {
    if (realmConfiguration == null) {
      DiContainer.getInstance().getApplicationComponent().inject(this);
    }

    return Realm.getInstance(realmConfiguration);
  }
}

このクラスはRealmConfigurationの生成責務も持ち, テスト専用のRealmConfigurationを生成するメソッドが実装される.

また, Configurationの異なるRealmがアプリ内に生成されるのを避けるためにRealmFactoryはSingletonオブジェクトにする.

  // 製品用RealmConfigurationの生成
  public static RealmConfiguration newRealmConfiguration(@NonNull Context context) {
    return new RealmConfiguration.Builder(context)
        .name(REALM_FILE)
        .schemaVersion(REALM_SCHEME_VERSION)
        .migration(new RealmMigrator())
        .build();
  }

  // デバッグ用RealmConfigurationの生成
  @VisibleForTesting
  public static RealmConfiguration newDebugRealmConfiguration(@NonNull Context context, @Nullable String fileName) {
    return new Builder(context)
        .name((fileName == null || fileName.length() == 0) ? "test_" + REALM_FILE : fileName)
        .schemaVersion(REALM_SCHEME_VERSION)
        .migration(new RealmMigrator())
        .build();
  }

RealmConfigurationを後からDIでInjectするためにfactoryメソッドの公開範囲はpublicとしておく.

Rebuild component

よくあるApplication.onCreateでObject graphを構築する場合を考える.
コードの中で登場するDiContainerはObject graphを管理するコンポーネント.
Androidのコンポーネントをテスト環境から疎にするためApplication経由ではなくDiContainerを経由させる.

public class App extends Application {
  @Override
  public void onCreate() {
    super.onCreate();

    // よくある方法を使う
    DiContainer.getInstance().setApplicationComponent(
        DaggerApplicationComponent.builder()
            .applicationModule(new ApplicationModule(this))
            .build());
  }
}

Unit Test実行時にはデバッグ用のObject graphに再構築する必要がある.
デバッグ用にRealmConfigurationを作成するAPIは既に用意してあるため, DIでこれを差し替える方法を実装していく.

まずはRealmConfigurationをinjectするための本番用moduleを定義する.

@Module
public class ModelModule {
  @Provides @Singleton
  RealmConfiguration provideRealmConfiguration(@NonNull Context context) {
    RealmConfiguration configuration = RealmFactory.newRealmConfiguration(context);
    return configuration;
  }
}

続いてデバッグ用moduleを定義.

public class DebugModelModule extends ModelModule {
  @Provides @Singleton
  RealmConfiguration provideRealmConfiguration(@NonNull Context context) {
    RealmConfiguration configuration 
        = RealmFactory.newDebugRealmConfiguration(context, null /* test_ prefixed */);
    return configuration;
  }
}

あとはObject graphをテスト前に再構築すればデバッグ用のRealmConfigurationがinjectできるようになる.

public class DebugHelper {
  public static void setupDiContainer() {
    DiContainer.getInstance().setApplicationComponent(
        DaggerApplicationComponent.builder()
            .modelModule(
                new DebugModelModule())
            .applicationModule(
                new ApplicationModule(
                    (App) InstrumentationRegistry.getTargetContext().getApplicationContext()))
            .build());
    RealmFactory.getInstance().deprecateConfigration();
  }
}

他にもSchemeVersionを指定するテストであったり, encryptしてのテストなども同じようにmoduleを用意すれば事足りる.

Unit Testの実行前にはApplication.onCreateが実行される.
そのため, Object graphは一旦本番用のRealmConfigurationで初期化されることになる.
Unit TestのsetupでRealmConfigurationが差替えられるのはその後になるため, Application.onCreate内でRealmにアクセスするようなケースがある場合は対策を打つ必要がある.
(その場合はtestApplicationIdでパッケージを分ける方策を探るのが無難だと思う)

以上.

2015/10/17

Hubot on Heroku

What’s Hubot

GitHub製のBOT. CofeeScriptで書かれており, Node.jsで動作する. MITライセンスなOSS.
独自のscriptを定義でき, adapterの機構で様々なチャットシステムにも対応できる. ChatOps.
HubBotを動作させるにはRedisが必要.

Install Hubot.

Hubotを始める方法はこちらに詳しく書かれている.
HubotはNode.jsで動作するためNode+npmの環境を用意しておく必要がある.

HubotはYeoman generatorからinstallする.

# アクセス権限が必要であればsudoで.
npm install -g yo generator-hubot

次に, Hubot用のディレクトリを作成して, そこで新しいHubotインスタンスを作成する.
いくつか質問されるので回答する.

# 今回はボットの名前をmarimoで作成
mkdir marimo
cd marimo
yo hubot

# Bot adapterはとりあえずcompfireで
? Owner: MatsumuraYuki <xxx@gmail.com>
? Bot name: marimo
? Description: A simple helpful robot for your Company
? Bot adapter: campfire

実行が成功するとHubotに必要なrediaも同時にインストールされている.
Botの雛形が出来上がったのでgit repositoryにcommitしておく.

git init
git add .
git commit -m "Initial commit. Hello marimo!"

ローカルでmarimoを動かしてみる.

# shell adapterを使ってHubotを起動
bin/hubot

errorメッセージが表示されてがskipする. プロンプトがmarimo>になれば対話ができる状態である.
とりあえずpingで生存確認.

marimo> @marimo ping
marimo> PONG

pongの返答があればok. 他にも使える対話コマンドが多くある.

marimo> marimo help

Deploy Hubot

marimoをHerokuにdeployする.

事前にHeroku Account, Heroku App, Heroku Toolbeltを用意しておく.

# heroku appを作成. アプリ名は適宜変更. 今回はmarimo-appで作成
heroku create <app name>

Hubotがデータを永続化(brain)するのに必要なredisをHeroku側に用意しておく.

# 無料planの30MBタイプを指定. addon追加にはHerokuにクレジットカード要登録
# https://elements.heroku.com/addons/rediscloud#pricing
heroku addons:create rediscloud:30

heroku createローカルGit repositoryのremoteにherokuが追加されるので, remoteへhubot scriptをpushする.

git push heroku master

rootにあるProcfileを見てみる.

web: bin/hubot -a compfire

-a引数により, hubotのadapterがcampfireになっている.
他のチャットサービスと連携させたい場合はadapterを追加してこれを編集する.

Hello Slack

HubotをSlackと連携させる. Slackと連携させるためのadapterはすでに用意されている.

slack adapterをインストールする.

npm install hubot-slack --save

# installできたか確認
npm list hubot-slack
marimo@0.0.0 /Users/yuki312/marimo
└── hubot-slack@3.4.0 

Heroku deploy時に使われるscriptも変更しておく.

vim Procfile

# 下記内容に変更
#   before: web: bin/hubot -a campfire
#   after : web: bin/hubot -a slack

slackのintegrationにHubotを追加するとHUBOT_SLACK_TOKENが得られるのでこれをHeroku環境変数に設定.

heroku config:add HEROKU_URL=https://<Heroku App URL>
heroku config:add HUBOT_SLACK_TOKEN=xoxb-...

git commitしたらHerokuへdeployして完了.

git add .
git commit -m "support slack"
git push heroku master

以上.