fresh digitable

セミコロンたちが躍動する おいらのコードを 皆さんに 見せたいね

カジュアルにIdlingResourceを使う

  • UIテストをやる時にMockWebServerを使ってサーバの挙動をモックしていると、レスポンスをちゃんと待ち構えないとテストが失敗することが稀によくある*1
  • カジュアルにIdlingResourceを使ってカジュアルに待てるようにしたい
  • テストを書くためのハードルを下げたい

ポイント:

  • ある状態になるまで待ちたくなった時にregisterして所望の状態になったらすぐにunregisterする。そうせず大域的に使うといろんな条件がバッティングして失敗する。
  • registerしたらなにがなんでもunregisterされるようにしたいのでtry (catch) finallyでくくる
  • isIdleNow()のなかでActivityLifecycleMonitor.getActivitiesInStage(Stage)を使ってactivityを取得できれば勝確
    • ActivityScenarioとかActivityTestRuleとかをわたしてもよいかも?
    • 結構無茶なことをしているしIdlingThreadPoolExecutorとかを使ってうまくやるのがいいと思う
    • ダイアログとかRecyclerViewの状態を待ちたいときはこうするしかないような気もするがそもそもこんなことしなければならないのが間違っている気がする

コード例:

まずはIdlingResources。結局はisIdleNow()の挙動を実装できればいい。公式サイトにはisIdleNow()の中でIdlingResource.ResourceCallback.onTransactionToIdle()を呼ぶなと書いてあるが今回の用途ではここで呼ばないとテストが進まなくなることがあるので仕方なく呼ぶ。

fun createIdlingResource(name: String, block: () -> Boolean): IdlingResource {
    return object : IdlingResource {
        override fun getName() = name

        override fun isIdleNow(): Boolean {
            val isIdle = block()
            if (isIdle) {
                callback?.onTransitionToIdle()
            }
            return isIdle
        }

        private var callback: IdlingResource.ResourceCallback? = null
        override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
            this.callback = callback
        }
    }
}

fun waitWithIdlingResource(name: String, block: () -> Boolean, afterTask: () -> Unit) {
    val idlingRegistry = IdlingRegistry.getInstance()
    val resource = createIdlingResource(name, block)
    try {
        idlingRegistry.register(resource)
        afterTask()
    } finally {
        idlingRegistry.unregister(resource)
    }
}

先ほどのメソッドを作ってテスト対象のActivityが所定のStageにIdlingResourceを作ってみる。

inline fun <reified T : Activity> waitForActivity(stage: Stage, noinline afterTask: () -> Unit) {
    val name = "wait_for_${T::class.java.simpleName}_in_${stage.name}"
    waitWithIdlingResource(name, {
        ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(stage).firstOrNull {
            it is T
        } != null
    }, afterTask)
}

先ほどのwaitForActivityをリファクタリングしてFragmentが差し込まれるまで待つIdlingResourceを作ってみる。

inline fun <reified T : Activity> waitForActivity(
    stage: Stage = Stage.RESUMED,
    name: String = "wait_for_${T::class.java.simpleName}_in_${stage.name}",
    crossinline onActivity: (T) -> Boolean = { true },
    noinline afterTask: () -> Unit
) {
    waitWithIdlingResource(name, {
        val a = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(stage).firstOrNull {
            it is T
        } ?: return@waitWithIdlingResource false
        return@waitWithIdlingResource onActivity(a as T)
    }, afterTask)
}

inline fun <reified T : Fragment> waitForFragment(noinline afterTask: () -> Unit) {
    waitForActivity<FragmentActivity>(name = "wait_for_${T::class.java.simpleName}",
        onActivity = { a ->
            a.supportFragmentManager.fragments.firstOrNull {
                it is T
            } != null
        },
        afterTask = afterTask)
}

しなくていいならしないに越したことはないと思うけど、退っ引きならない事情でどうしてもやらなければならない時だけどうぞ。Activityを待つ奴を応用するとRecyclerViewに所望のViewが差し込まれるまで待つとかいうのも作れるようになるので興味のある方は挑戦してみてください。


最近読んだのでよかったらどうぞ。

peaks.cc

*1:結構すぐ返ってくるので待ってなくても割と成功する

Androidでマルチモジュールをやってみているという話

droidkaigiでマルチモジュール関連の発表をたくさん聞いて、やってみたくなったのでやってみている。

github.com

データレイヤーを単に外に出すところまではなんとなくできたけどそこから先のところでうまく行かなかったり腑に落ちたり落ちなかったりしている。基本的な設計力がないからマルチモジュール化できないということなのであろうか。どうしてそうなっているかを理解せず何かの真似でやってみてもうまく行かない。

ちなみにここでモジュールって言ったらGradle(とかKotlin)で言う所のモジュールのことです。

daggerがなんか難しい

  • モジュールとSubcomponentとを対応させればいいんじゃね?と考えて公式ページを見ながらやってみるものの説明の通り(?)にやってみたらStackOverflowError
  • ComponentをもっているモジュールがSubcomponentの依存関係に引っ張られて本来不要なモジュールに依存せざるを得なくなっている*1
    • kaptの挙動がよくないという説がある様子
    • apiのモジュールを挟めばいいんだろうか
  • koinとかはどうなんだろうと思い始める

スタイルとかテーマって開発中どうやって確認すればいいんだろう

  • マテリアルデザインのテーマが読み込めなくてFABを表示できない(?)何が悪いのかもよくわかっていない
  • styleとかthemeはsharedなモジュールに置く?スタイルと実装とは切り離して考えるべきだとも思うけどそこまでやるのもなんか違う気がしている。

Activity, Fragment, ViewModelはどう分けるのがよいのか

  • ViewModelはViewに依存しないとはいっても、どんなViewDataBindingにバインドされるかは知っているはず*2ではないかと思うので、レイアウトファイルとViewModelはなんとなくセットにしておきたい
    • なんだかんだ言って画面はFragmentを組み合わせて作ることになるので、Fragmentのレイアウトファイルと、それとバインドするViewModelとをセットにして作ることになるはず
    • そう考えるとFragmentとViewModelは一緒のモジュールに入れればいいんじゃないのか?と思わなくもない。でもモジュール化の意義的に正しいのか、なんとなく違和感を覚える*3
  • 一方で、ActivityのレイアウトファイルはFragmentのコンテナしか置かないとかいうやつがでてくる*4
    • そういうやつはバインドするデータやビューがあんまりないのでViewModelとバインドできてもおいしくない
    • そういうActivityはFragmentの置き換えを実装するだけでいいという感じになる。どんな時にどのFragmentを置くかというのをViewModelで管理することになるかもしれないが、FragmentManagerをいじるロジックはActivityに書くことになり画面が増えてくるとつらい
  • Fragmentの置き換えを専門にやるクラスがあればよいのか?→JetpackのNavigationがそれにあたるか
    • navigationを使えば、ActivityやFragmentは他にどんなActivityやFragmentがあるのか知らなくてよさそう
    • navigationだけがActivityやFragment、他のnavigationを知っていればよいというふうにできれば、navigation(の具象クラス)はapplicationと同じモジュールに置けばよい
    • 画面のことはenumとかsealed classで定義したクラス(sharedなモジュールに置く)で表現すれば、navigationの抽象クラスしか知らないクラスの中でも画面遷移を実現できそう
    • 画面遷移イベントの発行を行う処理はViewModelでやってよい
      • 画面遷移を依頼するようなイベントオブジェクトを定義するとMVIになるのか?

先は長い。俺たちの戦いは始まったばかりだ。

*1:build.gradleのdependenciesに列挙しなければならないという意味で

*2:特定のViewDataBindingにバインドされるのを前提にして作るはず

*3:ViewModelからFragmentを見えなくしたいからモジュール化するのかなとも思う

*4:ボトムナビゲーションとかドロワーとかがあるやつは知らん

JavaとKotlinが混在するプロジェクトでmockitoをmockKで少しずつ置き換えていく

前提

  • Javaのプロジェクトに徐々にKotlinのコードを増やしていっている
  • Javaのテストコードではmockitoを使ってモックオブジェクトを作成していた
  • テスト対象のクラスに注入するクラスがKotlinに変わった
  • Kotlinのクラスはmockitoではそのままモックできないのでなんかやる必要がある
  • mockitoの設定をなんかやるよりはmockKを入れてテストコードをKotlinにする方を頑張ったほうがよさそうと判断して、テストコードをKotlinにすることにした
  • JUnit5やSpek2の時代が来そうなのでその前に書いて供養したかった

方針

  • かといってテストクラスを全部書き換えるのは大変
  • 書き換えたいのはモックを作るところだけ
  • モックを作る処理を抽象化して、テストクラスからはどんなモックライブラリを使っているのか意識しなくてもよいようにしてしまう
  • テスト対象の生成はTestRuleの中で行なっており、そこでモックも一緒に作成していたので、TestRuleをそのままモックライブラリのラッパーとして使うことにした
    • TestRuleのプロパティにアクセスできる別のPageObject的なクラスに移譲しても良かったかも

どうやったか

  • テストクラスの静的な内部クラスとして実装していたTestRuleだけをktファイルに移動させてKotlin化
  • mockitoをmockKに置き換え
  • verifyとかのモック用アサーションTestRuleクラスに移行して、さらにmockKに置き換え
    • モックの生成を一回だけにしたかったらcompanion objectの中で生成するといいかも。その場合はfinishedとかで忘れずにclearMocks()をやる
  • 公式にTruthを入れる動きがあるのでアサーションライブラリのクラスも隠蔽しておくといいかも

おまけ

  • こんな感じにするとほぼすべての行がrule.から始まったりしてテストコードとしては何となく微妙
  • Kotlinのテストクラスならwith(rule) {}で消せる

これが

fun test() {
  // setup
  rule.setupHoge(args = 42, returns = true)

  // exercise
  rule.testTarget.hoge(42)

  // verify
  rule.verifyHogeRepositoryIsCalled(args = 42)
  assertThat(rule.testTarget.isVisibleHoge.get()).isTrue()
}

こう

@Test
fun test(): Unit = with(rule) {
  // setup
  setupHoge(args = 42, returns = true)

  // exercise
  testTarget.hoge(42)

  // verify
  verifyHogeRepositoryIsCalled(args = 42)
  assertThat(testTarget.isVisibleHoge.get()).isTrue()
}

(2019/4/2 修正) IDEのラベルが荒ぶるので戻り値を書いている。 Unitを返すメソッドじゃないとテストが実行できないようなので、: Unitは念の為書いておく方が良い(少なくともこの例では必須)。