fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

重い腰を上げてテストのないクラスのテストを書くときまず最初にやること

テスト書くの後回しにしちゃったなーでもこの関数のテスト書かなきゃ…みたいなとき、まずどうするか。大事なことは最初のハードルを下げたり、ステップを小刻みにすること。

とりあえず最初はテスト対象のインスタンスを生成してそれがNonNullであるというテストを書く。つまりインスタンスを正しく作れるかというテストで、必要なことはコンストラクタに渡すインスタンスをどうやって作るかということだけである。だけなのだが、面倒なことがいくつかある。例えば、モックを作って渡すなら初期化時に呼ばれる関数の戻り値を設定しなければならない。そのほかにも、AndroidならContextなどプラットフォームのクラスを中で使っている場合はAndroidJUnit4をテストランナーとして設定する必要がある。うっかりLog.d()が入り込んでいたりするとTimberに置き換えるといったような別の作業も発生するかもしれない。

最初にやらなきゃいけないことが実はたくさんあるので、いきなり本題のテストを書き始めようとしてもなかなか進まずイライラすることになる。また、これらの作業を経ることによってimportがいい感じに温まっているという副産物もある*1

最初のテストが書けたら、次は書こうと思っていた関数のテストのうち、もっとも単純なケースのテストをとりあえず2・3個書いてみる。TDDでレッドとグリーンの間を行ったり来たりしてもよい。きれいなコードを書く必要はなく、コピペしてテストの関数名と渡すパラメータを少し変えたようなものでよい。いくつか書いていくと、自分が本当はどんなことをこのテストで確認したいと思っているのかということや、テストに共通で登場する前提条件や処理群といったものが明らかになる。特に共通する処理の最たるものというのはほかでもなく、最初に書いたNonNullテストの中の初期化処理である。これをひとまずはsetupのための関数として使えるように、TestアノテーションをBeforeアノテーションに置き換え、生成したインスタンスを各テストケースからアクセスできるようプロパティにセットする*2

あとはコピペで作ったテストから重複するコードを取り除いたり、いっそのことパラメタライズドテストに書き換えてみたり、テストクラスが大きくなったら前提条件ごとに内部クラスに分割したり、どの内部クラスでも使うRuleをまとめて使えるようにしたRuleを作ったりしながらテストを育てていけばよい。

まとめ

こんなブログを読むよりもレガシーコード改善ガイドとかを買って読んだ方が明らかに役に立つ。

*1:テンプレートを作っておけという話もある。

*2:Java的なアプローチですね。Kotlinならlazy {}の中に押し込んでもいいのかもしれない。テストの前提条件がいい感じにまとめられるとよい。

RobolectricのSQLiteの実装がPRAGMA defer_foreign_keysをサポートしていなかった

androidTestで使うDBのデータをセットアップするという記事を以前書いた。

akihito104.hatenablog.com

今回はこれをJUnitテストでも使えるようにしようとしてはまったポイント。

今回の前提:

testImplementation 'org.robolectric:robolectric:4.3'

言いたいことはタイトルですべて言っているので、どう対応したかということについて説明する。といってもそんなに難しいことはなく、単に PRAGMA defer_foreign_keys;を実行して戻り値があるかどうかを確認するだけである。

val isDeferForeignKeysSupported = supportSqliteDatabase.query("PRAGMA defer_foreign_keys;").use { cursor -> 
  cursor.count == 1
}
if (!isDeferForeignKeysSupported) supportSqliteDatabase.execSQL("PRAGMA foreign_keys = false;") // これは使えるらしい

supportSqliteDatabase.beginTransaction()

if (isDeferForeignKeysSupported) supportSqliteDatabase.execSQL("PRAGMA defer_foreign_keys = true;")
...

これでandroidTestでもJUnitTestでもDBにデータをロードしてテストできる。もっといいやり方があれば教えてください。

[2020/12/22 追記] robolectric 4.5-alpha-1以上なら大丈夫かもしれない akihito104.hatenablog.com

NavigationとMaterialContainerTransformを使う

github.com

なんかいろいろあったのでメモ。

今回の前提

implementation "androidx.navigation:navigation-fragment-ktx:2.3.0-alpha06"
implementation "androidx.navigation:navigation-ui-ktx:2.3.0-alpha06"
implementation "com.google.android.material:material:1.2.0-alpha06"

今回はListからDetailに遷移するときにContainer transformを入れてみることにした。ドキュメントに沿ってMaterialContainerTransformクラスを使って実装すれば大体のところまではできる。

material.io

material.io

引っかかったいくつかのところについてまとめておく。

遷移するときにListとDetailの表示が重なってしまう

Detail(上側)のViewがそれっぽく動いているのは見えるんだけどList(下側)も見えてしまうという感じ。背景の色を指定してやればよい。MDC初心者だったのでいっぱい調べました。

containerColor = Color.WHITE

ListからDetailはアニメーションするけど逆のときはしない

ドキュメントにはList -> Detailの実装のことしか書いていないし、これで逆向きのアニメーションもOKというようなこともそれとなく書いてある。実際にはそうなんだけど、Navigationコンポーネントと一緒に使うときには気を付けなければならない。issueにも挙がっているし一つ目のコメントに答えが書いてある。ざっくりいうと、Navigationコンポーネントで画面遷移するときに実行されるFragmentTransaction.setReorderingAllowed(true)が原因らしい。

[MaterialTransformContainer/Motion] sharedElementReturnTransition not working with navigation component · Issue #1073 · material-components/material-components-android · GitHub

setReorderingAllowed(true)は普通のshared elementアニメーションだと必要(参考: Android Developers Blog: Continuous Shared Element Transitions: RecyclerView to ViewPager )らしいのだけど、MaterialContainerTransformでは今のところ逆にうまくいかないらしい。ワークアラウンドについては次のissueが詳しい。

Navigation component: shared transitions backward not works · Issue #495 · android/architecture-components-samples · GitHub

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    postponeEnterTransition()
    view.doOnPreDraw { startPostponedEnterTransition() }
    ...
}

リストの一番上のアイテムだけDetailが半分になってしまう

それっぽく動いているように見えるが、

先頭のアイテムだけアニメーションがおかしい。

横から入ってきているように見えるが、実際にはアニメーションの最中だけViewが左側にずれてしまっている。モーションパスの始点と終点が同じだと動画のようになってしまうようす。苦し紛れにモーションパスを作るPathMotionクラスを実装して渡したら何となく動いたので、今のところのワークアラウンドとしておく。

https://github.com/akihito104/nasnen/pull/1/files#diff-cbd23c4e8d257b7f5cff6fa3da5bcdf9R42-R51

こういう使い方は想定していないのだろうか。例を見るとDetail側にはToolbarもContainerの中に入っているので、ここで見たような不具合は起きなさそうではある。