なるようになるかも

力は多くの場合、その人の思いを超えない。

mockk における引数を使った検証の話

Android テスト全書は Mockito Kotlin 推しだったので、しばらくはそっちを使ってたのですが、Coroutines への対応度とか、DSL を使ったモックや検証の記述は mockk の方が圧倒的にやりやすいので乗り換えました。

この乗り換えで引っかかるポイントとして、Mockito の場合、verify のタイミングで引数のキャプチャが可能でした。

@Test fun test() {
  viewModel.loadUser()
  argumentCaptor<UserId>().apply {
    verify(userRepository, times(1)).fetchUser(capture())
    assertThat(firstValue.value).isEqualTo("1")
  }
}

mockk だとモックを作るタイミングで CapturingSlot などでキャプチャしておく必要があるため、@Before にモックの組み立てを記述して、検証と分けておきたい場合などに、テストコードが肥大化しやすく、絶妙なストレスポイントになっていました。

private val userIdSlot: CapturingSlot<UserId> = slot()

@Before fun setup() {
  userIdSlot.clear()
  userRepository = mockk { fetchUser(capture(userIdSlot)) }
}

@Test fun test() {
  viewModel.loadUser()
  verify(exactly = 1) { userRepository.fetchUser(any()) }
  assertThat(userIdSlot.captured.value).isEqualTo("1")
}

公式ドキュメントの Matchers の一覧に match というものが存在し、任意の述語を書けることが分かったので、当初はこれを使っていました。単純なものであればこれで問題ないと思います。

@Test fun test() {
  viewModel.loadUser()
  verify(exactly = 1) { 
    userRepository.fetchUser(match { it.value= "1" }) 
  }
}

複雑な条件をテストしようとすると match では可読性が落ちる懸念や、assertThat() にある検証ロジックを使いたいケースもあったことから、いろいろ模索した結果、最終的には withArg を使う方法に落ち着きそうです。

この Matcher は、

matches any value and allows to execute some code

あらゆる値とマッチする(Matcher としての意味は any() と同じ)ものの、受け取った引数を使って任意のコードを実行できるというもので、最初この存在に気付かなかった上に(verification mode のみで使える Matcher として別の表になっていた)、これだけを読んでも使いどころがピンとこない感じですが、要は assertThat() をここで書けばいいのです。

@Test fun test() {
  viewModel.loadUser()
  verify(exactly = 1) { 
    userRepository.fetchUser(withArg{ 
      assertThat(it.value).isEqualTo("1")
    })
  }
}

検証時に値を取得してアサーションを書けるので、Mockito の ArgumentCapture と同じような使い勝手になります。

groupie の差分更新でうまい感じにアニメーションさせる

groupie が便利というよりも、素の RecyclerView が不便すぎるというべきだと思いますけれど、楽ですよね。

github.com

groupie はテキストベースのリファレンスは充実していないので、まずはサンプルコードをちゃんと読みこむ必要があります。そうすれば、特に実装に困ることはないと思います。

差分更新をちゃんと機能させる方法について、公式サンプルの UpdatableItem.kt を読めばおおよそ事足りるのですが、解説を書き残してみます。

モデルクラスをちゃんと作る

まず、モデルクラスは純粋なデータのみを持った data class とすることが望ましいです。

すくなくとも、コンテンツの中身が書き換わったということを検知するためには、モデルオブジェクトの同値性が正しく実装されている必要があります。data class を用いれば自動的に実現されます。

このとき、モデルクラスは自身を識別する一意なキーを持っていると望ましいです。

data val Model(val id: Int, val text: String)

getId() を override した BindableItem を実装する

Item#getId() を override して、同一のモデルであることを一意なキーで示します。

hashCode()equals() はモデルクラスの同値性と同じになるようにします。BindableItem 自体を data class にすれば、これも自動的に実現できます。

data class MyItem(val model: Model) : BindableItem<ItemModelBinding>() {

    override fun getLayout() = R.layout.list_item_model

    override fun bind(viewBinding: ItemModelBinding, position: Int) {
        viewBinding.model = model
    }

    override fun getId(): Long = model.id.toLong()
}

これで、追加・更新・削除のすべてのパターンで適切にアニメーションされるようになります。

パターン 結果
アイテムが追加された 既存リストに同一の ID がないので追加と判定される
アイテムが更新された ID は同一だが、同値ではないので更新と判定される
アイテムが削除された 新規リストに同一の ID がないので削除と判定される

付加的なオブジェクトを渡したい場合

BindableItem にモデル以外のオブジェクトを渡す必要があり、data class の備える同値性が壊れてしまう場合、data class をやめて、以下のように equals()hashCode() を明示的に実装します。

override fun equals(other: Any?): Boolean = model.equals(other)
override fun hashCode(): Int = model.hashCode()

Android Kotlin のアサーションは Truth が良いのでは?

Android テスト全書を読んでたら、「Kotlin でアサーションを書くなら AssertJ」って感じだったので。

確かに AssertJ は堅実で、現実的な選択肢だと思うんですけれど、ちょっと気になったきっかけが P42 の AssertJ Android のコラムで、「サポートライブラリの進化への追従が難しかったから廃止された」とだけ書いてあって、もうひとつの理由については触れていないこと。

Additionally, we no longer think AssertJ's model for supporting alternate assertions is a good practice.

We recommend using Truth which has vastly superior extensibility model which, when coupled with things like Kotlin's apply method create a really nice assertion experience.

引用元: square/assertj-android

超ざっくりですが、「もはや AssertJ が代替アサーションを提供する良い手段だとは考えていない」「より優れた拡張を持ち、Kotlin との親和性が優れたアサーション体験をもたらす Truth を推奨する」みたいなことが書いてあります。

これを書いたのは Google 入社直後の Jake 神なので、Google 製の Truth を推しただけかもという可能性はありますが、かなりベタ褒めされてます。

で、Truth って何?

google truth」という単語の組み合わせでググっても、「グーグルの検閲によって真実が歪められている!」みたいなのが引っかかって、全然ライブラリの感想が出てこなくてぐったりしますが、Google の Guava 開発チームが作ってるアサーションライブラリです。

google.github.io

Android テストを書く上で Truth を採用するメリットは?

AndroidX Test Library が Truth 向けの拡張を用意している、というのが大きなメリットかなと思います。

AssertJ Android が突然の死を迎えたように、サードパーティの便利なライブラリは、依存しすぎると将来的にどうしようもない負債になるリスクがあるので、公式でサポートされている、というのは安心要素です。

とはいえ、現時点では対応しているコンポーネントはあまりないのですが、AndroidX Test Library はオープンソースなので、そのうちコントリビュートが殺到して充実するかもしれないし、そうでないかもしれないです。

どんな感じの書き方なの?

AssertJ と大体同じです。

大きな違いとしては、AssertJ だと method chain で複数の検証を連結させることができましたが、Truth ではこの方法を避けています。

Java だとやや冗長になるのですが、Kotlin には apply があるのですっきり書けます。

assertThat(actualIntent).apply {
    hasComponentClass(UserPageActivity::class.java)
    hasFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
assertThat(actualIntent.extras).apply {
    isNotNull()
    string("KEY_URL").matches("https://example.com/user/[a-z]{3,12}/")
}

拡張性は良いの?

AssertJ Android と AndroidX Test Library の実装を比較してみた感じ、より簡単に作れそうな感じはします。

IntentFlag 周りの実装が端折ってるので、出力されるエラーメッセージ自体は、AssertJ Android の方が親切ですね。

まだ安定版がないので導入は早いのでは?

現在のバージョンは 0.42 で、確かに安定版ではありません。

1.0 のリリースはいまは 2019 年の秋を予定しているらしいです。

こう聞くと AssertJ の後継として作られた、開発が活発な新しいライブラリなのかな? と思うところですが、そうではありません。

AssertJ とよく似た API をしているのは、AssertJ の fork 元である FEST に触発されて開発されたものだから……という理由らしく、2011 年から細々と存在しているみたいです。(AssertJ は 1.0 が 2013 年にリリースされたのですが、それより前からあるらしい)

なお、散発的にアップデートしていますが、お世辞にも活発とは言いがたく、1.0 が本当に 2019 年に出るのか怪しいと思ってます。

もっとも、1.0 に向けた開発のフォーカスは、主に拡張を作る人向けの部分らしいので、アサーション部分に破壊的変更が入る可能性はそれほど高くないのでは、と思います。

正直どっちでも大して変わらないのでは?

それはそうなんだけど人柱を増やしていきたい。

RxSwift の実装例っていろいろありすぎて困るよねって話。

元ネタ

感想

他人のコードを読むとき、どういうフレームワークや言語でも、「この機能をこんな使い方するのか……マジかよ……」というのはいい意味でも悪い意味でもあると思うのですが(悪い意味の方が7割くらい)、Rx 関連になるとみんな実装方法がまちまちで、もはや何を参考にしていいのか分からない状態になりますよね。

RxSwift 界隈と RxJava 界隈とでも空気感が全然違ったりしますし。

個人的な考えとして、Rx + MVVM を独学で学ぶならば、習熟段階に応じて、

  1. BehaviorRelay を入れ物にして、PublishSubject でイベントを発火する似非 Rx で考え方に慣れる
  2. 副作用(状態)を減らすことを腐心し、関数指向を目指しつつ、いろいろ妥協もして動くものを作る
  3. 理想的な Rx + MVVM アーキテクチャに到達する(自分はしてない)

と進んでいくのが無難だと思っていて、元記事はサンプルコードの一部の「そもそも Swift としてどーなの?」っていう突っ込みポイントを除けば、「入門者への記事」としては、そこまで悪いとも思わなかったり。

Rx 全然分からない人に向けて、挫折しにくい、自分の書いたコードと動作がわかりやすく対応付くような、「最初の一歩を踏み出させるサンプルコード」を作成するのって、なかなかハードルの高いタスクだと思うんですよね。

私的な実装例

なんとなく自分も実装してみたり。とはいえ、おおよその実装は前述した記事のコードをパクった真似た上に、テストコードはないのですが……。(あとで書いた)

github.com

ViewModel の作り方だけは、わりと変えてます。

ViewModel を作るときに、「依存モジュール」「入力」「出力」がごっちゃにならないように、こういうプロトコルに準拠させてます。

protocol ViewModelType {
    associatedtype Dependency
    associatedtype Input
    associatedtype Output
    
    init(_ dependency: Dependency)
    
    func transform(_ input: Input) -> Output
}

イニシャライザで受け取るのは依存するモジュールだけにして、入力の Observable<T> を受け取り、出力の Driver<T> を返す transform() というメソッドを用意してます。ViewModel のコード全体はこちら

この手法、すっきりしていてわたしが私的にコードを書くときには好んで使うのですが、ものすごい複雑なケースだと破綻するっぽい?

記事のレベルと共感

以下、あんまり関係ない余談。

某ポエムサイトでは「エンジニアは全員記事を書くべき」「ゴミ記事で汚染するべきではない」みたいな論争をやってるっぽいですね。

わたしがプログラミングの記事を書いているのは、自身が初学者だったときにいろいろな記事にお世話になった恩返しをしたい……というのが動機としてあったりしました。

そうした頃に読んだ記事は、片っ端からブックマークに入れていて、今でもたまに読み返しているのですが、

  • この実装よりももっといい方法があるなー
  • なんでこんなことで悩んでいたんだろう?
  • 当時は理解できなかったけれど、こういうことだったのかー
  • まだ難しくて読めない……

みたいに、自身に知識に応じて内容の理解度が変わって、新しい発見があって面白いです。

私見として、高度で正しい技術記事だけがこの世にあるよりも、間違ったり迷走したりしてるような記事も含めて、玉石混合の記事がある方が良いと思っています。

というのも、記事への共感というか、理解しやすさというのは、読み手と書き手のレベルが最接近しているときが最大になるという体感があって、中級者以上の人にとってはどうでもいいような悩みやトラブルの記事もきっとどこかの誰かの役に立つし、逆に「いいね」がまったく付かない内部実装について詳述したようなマニアックな記事もどこかの誰かにとっては最高の読み物になると信仰してます。

なので、明確な誤りに対する指摘は有益だと思いますが、「このやり方が正しい」という押し付けについてはあまりしたくはないんだけど、コードレビューをお仕事でしてるとそれに近いことをやってしまうことがあって、なかなか難しい……。

最近のアプリ界隈での「設計」の違和感

アプリ界隈で「設計」の話をするときに MVC / MVP / MVVM のような「設計パターン」だけが語られるようになった気がする。

往々にして、「アプリの規模によってどれを採択すべきかは変わる」みたいなお茶を濁すような結論で終わることが多い。

私的な結論

  • 「設計」と、「設計パターン」は別物だと思う。
  • 「設計」のレベルを上げたい。
  • アーキテクチャシンドロームから抜け出して、価値のあるものを作りたい。

以下、思うところのメモ。

MVC は古い / 劣ったやり方か?

MVC は Model をどう構築するかについてとくに規定していない。

MVC への批判をするときに、FatVC が持ち出されることが多いのですが、FatVC を実装してしまうのは単に実装者の能力不足だと考えていて、MVVM を採用しても FatVM を作るだけだと思っている。

また、比較的新しめの Flux アーキテクチャは、良く言えば MVC を詳細化したもの、悪く言えば MVC の再発見とみなすことができる。

VC / Activity が GOD クラスであることから目を背けるのはよくない

VC や Activity をクリーンにすることは重要なものの、そこに熱中しすぎるのはよくないと考えている。

VC や Activity は単一の View における状態の管理と、それらの View 同士の遷移の状態の管理を担っているため非常にやれることが多く、また、管理している状態も極めて複雑である。

これを上手く抽象化してパターンに当てはめるという試みが多数行われているものの、基本的にはトレードオフであり、正解はない。

もともと単体の VC や Activity で簡単にできたことがとてもとても複雑になってしまうというのはまだマシなほうで、インタラクティブ画面遷移のような「View の状態」と「遷移の状態」の両方を扱う操作は欠落してしまうようなケースもある。

GOD クラスの影響を低減させる試み自体は概ね好ましいものの、そこで消耗してもアプリの価値が劇的に向上することはないので、ほどほどに割り切って上手く付き合っていくのが良いと思っている。

DDD や Clean Architecture を設計パターンと同列に語る

MV* は実装フェーズ、それも GUI 実装のみに限定されたパターンの話でしかないはず。

対して、DDD はソフトウェア製造全体に対する巨視的な考え方をしているものだと思うし、Clean Architecture はアーキテクチャを構築する上での指針というか、設計パターンに対するメタな話だと解釈してるのですけれど、なんか同列で語られることが多いのが個人的にはもにょい。

あと DDD や Clean Architecture を独自解釈(というか、ネットの情報ベースで語っている?)ことについて思うところがないわけではないのですが、まだ原著完全に読めてないのでとりあえずノーコメント。

MVVM without DataBinding

まともなデータバインディング機構がないにも関わらず、MVVM が採用されるパターンが多い。自分もやる。

必ずしも悪いわけではないものの、以下のような副作用は散見される。

  • 双方向バインディングの記述がひどくぎこちない書き方になる
  • 一行のデータ更新によってリストを全て書き換えるような、大富豪な処理が平然と実行される
  • ひとつのボタン UI に対して、「状態に応じたボタンのラベル」「ボタンを実行するかどうか」「ボタンで実行する処理」のような複数のバインディングが必要になる
  • コンパイル・実行時エラーが不親切でデバッグ作業が非効率化する
  • データバインディングの作法が統一されていないので、オレオレ VM が横行する

個人的に「設計」をやる人に望むこと

要件に対する興味と理解

「設計」をやるときに GUI をどういうパターンで組むのか?から入るのは、ぶっちゃけよほど簡単で先の見通しがはっきりしている状況でもない限りは、悪手だと思う。

どういう要件があり、どういった機能を実現する必要があるのか?にまず興味を向けるべきで、ビジネスロジックドメイン層と言われる領域をうまく抽象化し、UI や DB などと隔離されたかたちで実装できるか?というところがもっとも肝要で、それこそが本来やるべき「設計」じゃないのかなぁと思っている。

プログラミングの原則の理解

設計パターンやアンチパターンの根底には大体プログラミングの原則の考え方が根付いている。

「BaseVC はアンチパターンだからダメ」みたいなのは単なる思考停止なのであまり好きではない。

たとえば「開放閉鎖原則の観点から、この実装では変更に対する柔軟性を失っており好ましくない BaseVC である」という指摘が入ると、具体的な問題箇所が明らかになり、どうやって修正するか?という建設的な議論に入ることができる。

設計パターンにはとりあえず手を出す

最終的に GUI を作る以上は MVC や MVVM や MVP などの GUI の設計パターンを採用することになる。

このとき、人や本を権威にして「MVC はダメ」「MVVM は実はこんなデメリットが」みたいに流されるだけではなく、自分で手を動かして実装するなり、コードリーディングはしなければならないと考えている。

というのも、作ったことも触ったこともないものに対して、どれが最適かなどという判断を下すことなど不可能なので。

ひとつの設計パターンに成功体験を持ってしまうと、その設計パターンがあらゆる状況で通用するものだと勘違いしてしまいがちだったり、その逆にも注意をする必要があると思う。

UISearchController を Rx で書けるように

iOS11 だと UISearchControllerUINavigationBar に入れられて楽しいので、入力した文字列をうまい感じで Observable<String> にしてくれる DelegateProxy を作ってみた。

UISearchResultUpdating+Rx の私的実装

なんで RxCocoa にないの?

PR 自体は何度か出てたけど蹴られてたっぽい。

./scripts/all-tests.sh 通してないなこの PR …。

ちゃんと RxSwift の作法っぽい感じで書き直したらこんな感じです。

これはちゃんと ./scripts/all-tests.sh も通ります。

よほどメジャーな delegate じゃない限り、この手のラッパーは各自で作ってねってスタンスだと思うので本家に PR は出さないですが。

UISearchController の使い方が分からない場合

ググってコードを探すよりも、Table Search with UISearchController というサンプルコードを見ると早いです。

なんと、Swift 版があるだけでなく、iOS11 対応済。

hidesSearchBarWhenScrolling が動作しない

UIScrollView と連動して動作してくれる条件は、以下の通り。

  • UIViewController の view が UIScrollView
  • UIViewController の view の subviews[0] が UIScrollView

Shrink large title when scrolling (not UITableViewController) iOS 11 に図解入りの超分かりやすい説明があります。

作った経緯とか

最初、UISearchResultsUpdating に準拠しなくても、searchController.searchBar.rx.value を subscribe すればいいんじゃ?と思ってたんですけれど、これだとうまくいかなかった。

  • キャンセル時に next が流れて来ない
  • 検索中に別の VC に push すると、ナビゲーションバーが消え去る

後者について、UISearchController が内部的に modal を出してるっぽいので、これを消すワークグラウンドとかを思いついたのですけれど、正規の方法で実装した方がよさそうだなぁということで素直に実装しましたとさ。

ついでだったので、SearchBar に入力したテキストを取れる Observable を追加したのですけれど、ちゃんと動いてるっぽい。

var queryText: Observable<String> {
    return Observable.merge(Observable.just(""),
                            RxSearchResultUpdatingProxy.proxy(for: base).didUpdateSearchResultSubject.map { $0.searchBar.text ?? "" })
        .distinctUntilChanged()
}

空文字と merge しているのは、combineLatest でフィルタリングしている実装の都合上です。

Activity Tracing どう使うのか謎だったので調べたメモ。

static void hoge() {
    static os_log_t log;
    static os_activity_t activity;
    static os_activity_t activity2;
    static os_activity_t activity3;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        log = os_log_create("activity", "a butterfly's dream");
        activity = os_activity_create("dream", OS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT);
        activity2 = os_activity_create("real", activity, OS_ACTIVITY_FLAG_DEFAULT);
        activity3 = os_activity_create("dream", activity2, OS_ACTIVITY_FLAG_DEFAULT);
    });
    
    struct os_activity_scope_state_s state = {};
    os_log_info(log, "Graveyard Memory");
    os_activity_scope_enter(activity, &state);
    os_log_info(log, "Nightmare Counselor");
    struct os_activity_scope_state_s state2 = {};
    os_activity_scope_enter(activity2, &state2);
    os_log_info(log, "Historical Vacation");
    struct os_activity_scope_state_s state3 = {};
    os_activity_scope_enter(activity3, &state3);
    os_log_error(log, "Cosmic Horoscope");
    os_activity_scope_leave(&state3);
    os_activity_scope_leave(&state2);
    os_activity_scope_leave(&state);
}

os_activity_scope_state_s は空の構造体を作って os_activity_scope_enter にポインタを渡せばいいだけなのね。

f:id:quesera2:20180429190527p:plain

こんな感じで階層表示される。

block でネストしても良いのであれば、os_activity_initiate マクロだと、Activity の作成+スコープ開始を同時にやってくれるので便利なのだけれど、Activity Tracing のリファレンス には載っていない…。

os_activity_initiate("dream", OS_ACTIVITY_FLAG_DEFAULT, ^{
    os_log_info(log, "Northeast Nostalgia");
    os_activity_initiate("real", OS_ACTIVITY_FLAG_DEFAULT, ^{
        os_log_info(log, "Forgotten Paradise");
        os_activity_initiate("dream", OS_ACTIVITY_FLAG_DEFAULT, ^{
            os_log_error(log, "Shining Future");
        });
    });
});

OSLog が記録しているファイル名や行数の情報を知りたい

Don’t include symbolication information or source file line numbers in messages. The system automatically captures this information.

「OSLog は自動的にソースファイル名と行数を収集するから、そういった情報をメッセージに含めるべきではない」というリファレンスの記述に対して、じゃあどうやって見ればいいの?っていう疑問が当然あると思うんですけれど、何も書いてないのですよね…。

Developer Forum を調べたところ、os_log source file and line numbers に記載を発見。

log コマンドを --source オプションを付けて実行したときに見れるっぽい。DevForum だと Swift だと動作しないという話があったけれど、今はちゃんと Swift コードでも行数が出るっぽい。

関数でラップすると、出る行数はラッパーになるので、やはり os_log を直接呼ぶしかないっぽい。@inline(__always) を使ってもダメだった。

$ log stream --level debug --source --predicate 'subsystem == "activity"'
2018-04-29 21:34:40.264429+0900 0x28b38e   Info        0x0                  31286  <Activity`hoge (hogehoge.c:28)> [activity:a butterfly's dream] Graveyard Memory
2018-04-29 21:34:40.264499+0900 0x28b38e   Info        0x786a3              31286  <Activity`hoge (hogehoge.c:30)> [activity:a butterfly's dream] Nightmare Counselor
2018-04-29 21:34:40.264540+0900 0x28b38e   Info        0x786a4              31286  <Activity`hoge (hogehoge.c:33)> [activity:a butterfly's dream] Historical Vacation
2018-04-29 21:34:40.264599+0900 0x28b38e   Error       0x786a5              31286  <Activity`hoge (hogehoge.c:36)> [activity:a butterfly's dream] Cosmic Horoscope
2018-04-29 21:34:40.264848+0900 0x28b38e   Info        0x786a6              31286  <Activity`__hoge_block_invoke.3 (hogehoge.c:40)> [activity:a butterfly's dream] Northeast Nostalgia
2018-04-29 21:34:40.264918+0900 0x28b38e   Info        0x786a7              31286  <Activity`__hoge_block_invoke.6 (hogehoge.c:40)> [activity:a butterfly's dream] Forgotten Paradise
2018-04-29 21:34:40.264982+0900 0x28b38e   Error       0x786a8              31286  <Activity`__hoge_block_invoke.9 (hogehoge.c:40)> [activity:a butterfly's dream] Shining Future
2018-04-29 21:34:40.269216+0900 0x28b38e   Default     0x0                  31286  <Activity`main (main.swift:26)> [activity:a butterfly's dream] Dominated Realism

os_activity_initiate の方が圧倒的に簡潔に書けるのですけれど、block を開始した行数しか記録に残らないので、os_activity_scope_enter を使った方がいいのかもしれない。