2019年のSwiftモック事情

ikesyo氏:では「2019年のSwiftモック事情」というタイトルで発表させていただきます。よろしくお願いします。

まず自己紹介です。ikesyoと言います。ふだんは京都のはてなという会社で働いていて、スマートフォンアプリの開発の仕事をしています。専門はiOSで、Swift大好き人間なんですがAndroidやReact Nativeも書いたりしています。swift-corelibs-foundationというSwiftコンパイラとは別のエリアなんですが、Swiftのコミッターの権限も持っていたりします。

あとはテストの話で言うと、QuickとNimbleのDDDのテストライブラリのメンテナーもしていまして、『iOSテスト全書』のQuick/Nimbleの章では若干レビューもさせていただきました。

今日は最初にモックの話なんですが、去年のiOS Test Night #7でDeNAのSWETの細沼さん(@tobi462さん)が「SwiftにおけるMockライブラリの活用」という発表をされていまして、モックがどういうものかや、前提的な知識はこちらで発表されているので、こちらもぜひ合わせて読んでいただければなと思っています。わりとこれを前提として細かいところは飛ばして話していこうかなと思っています。

ではモックなんですが、Swiftでモックをどう書くのかというところですね。Swiftは静的な言語で、基本的にはランタイムでいろいろやって黒魔術でモックするみたいなことはできないので、手で書くのか、コード生成をするのかが基本的な選択肢になります。

ということで、手で書く以外にコード生成の選択肢として今回4つ挙げていて、今年2019年に新顔として登場したものが1つあるので、それも紹介していきたいと思います。

1つ目はSourceryというツールのAutoMockableというものです。まずSourceryというツールなんですが、これはSwift用の汎用的なコードジェネレーターになっています。プロジェクトに導入してソースコードの解析をして、そのソースコード中にSourceryがサポートする形式でカスタムのアノテーションを書いて、それをテンプレート側に渡すことでコード生成ができる仕組みになっているツールです。

ソースコードの解析にはSourceKittenというライブラリを使っていて、テンプレートにはStencilというものが使われています。こちらのツールに関しては2018年の「try! Swift Tokyo」で作者の方が発表されていたので、ぜひこちらも見てみてください。

AutoMockableの役割

こちらのSourceryは汎用的なコード生成のツールなんですが、デフォルトで用意してくれているテンプレートにAutoMockableというものがあります。

これはSwiftでモックのコードを書くためのテンプレートになっています。プロジェクトのプロトコルにおいてAutoMockableという名前のプロトコルに準拠させることだったり、あとはこれがSourceryのコメントによるアノテーションなんですけど、「// sourcery: Automockable」のコメントをプロトコルに付けると、そのコードがMockableでのコード生成の対象になります。

これはけっこうシンプルで簡単に導入できるものなんですが、どういう動きをサポートしているかというと、メソッドのモックに対しては呼ばれたかどうかしかわかりません。何回呼ばれたかはわからないようになっています。

メソッドを呼んだあと、呼ばれた引数をタブルにしてプロパティに保存するようになっているので、それによってどういう引数で呼ばれたかがわかるようになっているんですが、何度も呼んだときには順次上書きされていくので、どういう引数の組み合わせで呼ばれたかなど、そういう検査はしづらくなっています。

あとはメソッドで戻り値があるメソッドに関しては、プロパティにあらかじめ値を設定しておいてからメソッドを呼ぶことで、戻り値を返すものを変更することができたりします。あとはプロパティも同様なことができますが、全体的にシンプルで機能が足りない感じではあります。

わりと困るのが外部引数名が同一なんだけど型違いというメソッドのオーバーロードをしようとすると、この辺りををモックしたときに呼ばれたかどうかをチェックするためのプロパティの名前が衝突してしまってコンパイルできなくなるので、そういったオーバーロードはNGです。

また、標準ライブラリなど以外で外部のフレームワークをインポートすると思うんですが、そういったインポートをしているテストコードだと、ビルドが失敗したりするので、その場合は標準で用意されているテンプレートを自分でコピペしてきて、import文を書き加えるみたいなことをしないといけません。ちょっと面倒くさいですね。

SwiftyMockyの機能

2つ目はSwiftyMockyというツールです。SwiftyMockyというのは、先ほどのSourceryをベースにしたツールになっています。モック対象とするプロトコルを判別するには先ほど説明したAutoMockableをそのまま使用しています。

テンプレートがより高機能になっていて、かつ、どういうメソッドが呼ばれたとか何回呼ばれたか、そういう検証をする部分やverifyする部分がランタイムのフレームワークとしてテストターゲットにインポートするものが別途用意されていて、より高機能になっています。Sourceryベースなので、先ほどのAutoMockableのテンプレートからの移行がしやすいのではないかなと思っています。

最新のバージョンでちょっと変わりまして、以前はSourceryのコマンドラインツールをそのまま呼ぶ形になっていたんですが、現在の最新のバージョンではMockfileという専用の設定ファイルと、Sourceryコマンドをラップするswiftymockyというラッパーのコマンドが用意されて、それを使うようになっています。

機能としてはザッとこんな感じになっていて、プロトコルのモックができてプロトコルが継承されている場合もサポートしています。ただ、Swiftのクラスのモックはできません。あとはジェネリクスもサポートしていて、どういった形でコードが生成されるかは、こちらのリポジトリの中にあるHandling GenericsというMarkdownファイルに書いてあります。

あとはstaticメンバー、staticプロパティとかstaticメソッドもサポートされているのと、@objcのプロトコルもサポートされています。

コード生成する対象となるファイルがどんなところに含まれているのかはファイル単位やディレクトリ単位と分けて指定することもできますし、先ほどの「Sourceryだとテンプレートを書き換えないといけないんだけど」と言っていたインポートするフレームワークは設定ファイル上で指定可能です。SwiftyMockyのラッパーのコマンドを叩くと、そのソースコードも解析してどういったフレームワークでインポートしているのかを解析して、自動的にその設定ファイルの中身を埋めるというコマンドもあります。

Given、Verify、Performというメソッド

使い方としてはGiven、Verify、Performというメソッドを呼んでモックをしたり、それがどう呼ばれるかを検証する感じになっています。

実際の雰囲気はこういう感じです。これはちょっと違和感があって、Givenと大文字の関数で始まっていたりして若干Swiftっぽくない印象があるかなと思います。

この場合はどういう感じかと言うと、モックで1つ目のものはJohnnyという引数で呼ばれているときにはBravoという戻り値を返し、それ以外の引数で呼ばれた場合はKowalskyという戻り値を返す感じでモックを書いています。

次は2つ目のこれはwillReturnのところが可変長引数になっていて、これは値を複数与えるとメソッドの呼び出しの度に返す値が変わるモックを書けます。この場合は3つ渡して3回呼んで4回目になると1つ目に戻るという形ですね。

次はモックがどう呼ばれたかの検証の部分ですね。回数を指定したり何回以上呼ばれたかを書けますし、neverで呼ばれていないことも検証できます。どういう引数で渡ってきたかという条件の部分は値を渡してみると、.anyでどんな引数でもOKという形もできますし、.matchingで引数の値をクロージャでとってクロージャの中で自由に条件を書くこともできます。この場合は$0.count > 3というので引数が文字列なので文字列の長さをチェックする感じですね。

もう1つ、Performという関数があって、メソッドを指定してメソッドのボディが呼ばれたときにクロージャを任意に差し込める機能になっています。その中で引数を受け取ることもできるのでその引数を使って処理できます。

独自のジェネレーター「Cuckoo」

ということで、次は3つ目のCuckooというライブラリです。これは、今までのSourcery系とは違って独自のコードジェネレーターになっています。ですが、裏側としてはSourceKittenというライブラリを使っているのとStencilというテンプレートのライブラリを使っているのは同様になります。

機能としてはSwiftyMockyとだいたい似たような感じなんですが、Cuckooはクラスのモックをサポートしているのが1つ違うのと、こちらはstaticメンバーはサポートしていません。ジェネリクスは昔のバージョンだとサポートしていなかったんですが、最近のバージョンからサポートしているのと、Objective-Cのサポートも裏側でOCMockを使うことによってのサポートが最近追加されました。

だいたいSwiftyMockyと近い感じになってきたかなと思います。以前はジェネリクスがなかったりでSwiftyMockyのほうが使いやすかったと思っていたんですけど、だいたい似通ってきました。

構文は大文字から始まるメソッドとかがないので、よりSwiftっぽいかなという感じですが、コード生成するときのコマンドへの指定でディレクトリ単位の指定ができなくて、1ファイルずつ指定していかないといけないので、そこがちょっと面倒くさそうかなと思っています。

import文に関しては、どういうフレームワークをインポートしているのかはコマンドラインツールの中で自動的にチェックして生成内容に入れてくれます。メソッドとしてはthenやthenReturn、thenThrowのメソッドがあって、雰囲気はこういう感じになっています。モックを作ってstubメソッドに渡してクロージャの中でstubの変数が来るので、それをwhenに渡してメソッドを呼んだときにどういう動きをするかを書いていきます。

先ほどのSwiftyMockyとわりと雰囲気が近いかなと思います。「any〇〇」というのも先ほどのanyと同じような感じですね。こちらもstubのチェインができて、それで複数回呼んだり、あと一番下のやつは可変長引数の形ですが、これを呼ぶことでSwiftyMockyと同様に複数回呼んだときに順々に返していくことができるようになっています。

こちらもどう呼ばれたかという検証のところですが、どういう引数で呼ばれるかをorでつないで書いてやることもできますし、あとは回数もtimesやnever、atLeastOnceという形でどういう回数呼ばれたかがチェックできるようになっています。

あと、ちょっとおもしろいのがArgument captureという機能があって、メソッドやプロパティをverifyするときにArgument captureのcapture()メソッドの戻り値ををセットしてあげると、そのメソッドやプロパティが呼ばれたあとの状態で最新の値もそうですが、セットされた過去の値も全部取ることができます。

Mockoloはコード生成が高速

Cuckooは以上で、最後に4つ目の新顔のMockoloというものです。MockoloはUberが作っているライブラリになります。たしか今年の6月ぐらいに公開されてまだそんなに知られていないんじゃないかと思っているんですが、Mockoloを知っていたという方はどれぐらいいらっしゃいますか?

(会場挙手)

1名、2名……数名というところですね。あまり知られていなくて良かったです(笑)。Mockoloの売りなんですが、コード生成が高速というところが売りになっています。これはUberの巨大なコードベースでモック生成をするときに、そこが一番の問題になっていました。それを解決するためにそれがポイントになっているんですね。

これもCuckoo寄りで独自のコードジェネレーターになっているんですが、ソースコードのパーサー部分、パース処理の部分で先ほどのSourceKitten以外にSwiftSyntaxが選べるようになっています。ちょうど今月リリースされたばかりの1.1.0というバージョンからSwiftSyntaxのパーサーが使えるようになって、そのバージョンではデフォルトもSwiftSyntaxになっています。

あとは以前まではSwiftSyntaxだとSourceKittenに比べてパース処理がだいぶ遅い問題があって使われていなかったんですが、Swift 5.1になって最新のタグ付けされたバージョンでSwiftSyntaxがだいぶ高速化されたことで使われるようになったそうです。

こちらは今までのAutoMockableとかではなくて@mockableというアノテーションコメントで対象を指定するようになっています。これはSwiftの標準の言語の機能では「@〇〇」と書けるのでそちらに寄せているという感じです。オプションで変更も可能です。

機能としてはこういう雰囲気になっていて、クラスのモックができないというので、これはSwiftyMockyと同様な感じで、一応READMEには将来的にサポートしていくかもと書かれています。あとはObjective-Cのサポートはとくにありません。Objective-Cプロトコルもとくにサポートしていません。

インポートは自動解決されるのでとくに気にすることはなくて、どういうソースファイルを対象にするかはファイル単位かディレクトリのどちらかの排他的な指定になります。混ぜては指定できません。

先ほどのSwiftyMocky、Cuckooのようなかっこいいverify、matcherのような仕組みはなく、生成されたモックにどれが何回呼ばれているみたいなプロパティがあるので素朴にXCTAssertする形になります。

ちょっとここでパフォーマンスの話に入ります。先ほどコード生成が高速なのが売りと話したんですが、ドイツで開催されたUIKonf 2019でUberの開発者の方がこのツールの発表をされていたんですが、ここに結果が出ています。

Uberのコードベースで230万行のコードでプロトコルが1万1,000あるという、超巨大なコードベースなんですが、さらにコード生成した結果のモックが50万行と、モックも9,000個できあがるというやつで、Sourceryだと1.2時間、キャッシュがあっても340秒。Cuckooだと2時間以上掛かっていたのがMockoloの内部的な実装で直列に書いた場合に17秒。

最終的に最適化して並列処理もやった結果、5.3秒になったという異常な速さが出ていました。「これはUberの超巨大なコードベースだからなんじゃないの?」と思ったので、ちょっと自分の手元でも試してみました。

SwiftyMockyを導入済みのプロジェクトが手元にあったので比較してみようと思います。キャッシュなしで実行してみたところ15.3秒です。この時点で先ほどのMockoloの秒数を超えているので意外となかなか遅いと思ったんですが、これをキャッシュありにすると3秒になりました。これをMockoloでやると0.5秒という感じで、この程度の規模でもめちゃくちゃ速くなったので驚きです。

コード側に戻ると、こういうプロトコルが実際にこういう感じのモックとして生成されて。

こんな感じでnumSetCallCountとか〇〇Handlerを自分でチェックしたり上書きしする感じになりますね。

なので素朴にXCTAssertを書いていく雰囲気になります。

このへんをもっと高機能なものがほしいという場合はSwiftyMockyとかCuckooを使うことになると思いますし、とにかく規模が大きくてスピード命という場合にはMockoloを使ってやったほうがいいんじゃないかなと思います。

あとはassociatedtypeがあるプロトコルの場合どういうコードが生成されるのかと言うと、これはコメントのところでどういうtypealiasをモックに与えるかを指定できるようになっていますし、何も指定しなかった場合はAnyに落ちたりするようになっています。

なので、どういう機能が使いたいか、パフォーマンスがどれくらい必要かを考えて、要件に合って、かつチームの規模やメンバーのスキルに合わせた選択肢を選んでいただければなと思います。

ということで、楽しくモッキングやテストをしていきましょう! 以上です! あと、こういったものを僕と楽しく探求してくれる人を募集しておりますので、興味のある方は採用のほうをご覧ください。ということで以上です。ありがとうございました。

(会場拍手)