F# 向けモックライブラリPersimmockを試作
モックフレームワークやモックライブラリは.NET界隈にいくつかあって、特にF#にはFoqがあるわけですが、現状で満足か、と問われると微妙な顔持ちになります。
以下、1年半くらい前にFoqの行けてないところをzeclさんに尋ねたときの会話です。
使ってみるとすぐにわかることですが、SetupMethodとSetupの使い分けがだるい(し、型安全じゃない)点が最大にビミョイところかと思います。メソッドチェーンよりも (|>) でSetupしたいとかもありますが、それは難しいのかもしれません
— ぜくる (@zecl) 2016年10月18日
実際、Foqは(おそらく意図的に)緩めのAPIが定義されています。
例えばMock<IHoge>().Setup(fun mock -> <@ mock.Hoge(any(),any()) @>).Calls<string*int>(fun (_,x) -> x)
だと、mock.Hoge(any())
でもコンパイルに成功するはずです(テストは失敗する)。
.Calls<string*int>(fun (_,x) -> x)
も引数の型をみているわけではなく、<string*int>
というユーザの自己申告を信じています。
仕方がないといえばそこまでですが、もう少し型安全に挑戦してみても良いのではないか、という気持ちが少し芽生えます。
というわけでPersimmockというライブラリを試作しました。
解説?
https://www.nuget.org/packages/Persimmock/0.0.1
https://github.com/persimmon-projects/Persimmock
名称的にはPersimmonの系譜ですが、現状はPersimmon非依存です。 1年前にリポジトリだけ作って放置していたものを、謎の気力上昇によって2日くらいででっちあげた感じです。
このライブラリを1行で説明すると”ガワ変えのFoq”です。 どのくらいFoqかというと、動的生成部分やVerification部分のAPIはそのまま拝借した程度にはFoqです。
mock<IHoges> { method (fun mock -> mock.Hoge) call (fun (_, x) -> x) }
こんな感じで書けます。
コンピュテーション式なのは、ReflectedDefinition(true)
が使えるいい感じのAPIを思いつかなかったからです。
メソッドの引数の型を取るためにメソッドを直接とります。
FoqのMethod
とほぼ同一です。
mock<IHoges> { setup (fun mock -> mock.Get(It.IsAny(), It.IsAny())) hook (fun (_, x: int) -> x) }
いちおうFoqらしい書き方もできますが、基本推奨しません。 前者だとオーバーロードメソッドやsetterがうまく処理できないので緊急回避的に残した次第(どうやったらsetterを関数として取得できるんだ…?)。
hook
は名前が思いつかずとりあえずで付けたものなので、たぶんそのうち変わります。
mock<IFuga> { property (fun x -> x.StringProperty) returns (fun () -> value) }
プロパティは別APIで、returns
には即値を渡せません。
パフォーマンスと書きやすさとコンピュテーション式でオーバーロードできない問題を天秤にかけた結果です。
let xs = mock<IList<int>> { method (fun xs -> xs.Contains) args (any<int>) returns (fun () -> true) } xs.Contains(1) |> ignore Mock.Verify(<@ xs.Contains(0) @>, never) Mock.Verify(<@ xs.Contains(any<int>) @>, once)
args
で引数を渡します。
頑張って型安全みをだしてみましたが、かわりにジェネリックメソッドに対して無力になりやすいです…。
Foqのany
だと型推論が誤作動して2引数をタプルとして解釈し、実行時にエラーになりやすかったため、any
は型パラメータを明示する形にしました。
ただし、その分融通が利きません。
あえて型推論に身を任せたい場合はIt.IsAny()
を使います。
VerifyはFoqそのままなので特になし。
未実装機能
Mock.~ByName
は型安全にできないのであえて実装していません。
Mock.As
はなんか過剰な気がしたのと、現状の実装と相性が悪すぎたので実装していません。
まとめ
型安全にしようとするとどこかが割を食う、ということがよくわかる試作結果でした。 もう少しどうにかできないものか…研究は続きそうです。
Foqのバックエンドよくできているので、ぶっちゃけそんなに手を入れる必要はないと思っています。
余談
ここまで作っておいてなんですが、個人的にはinterface + オブジェクト式でやっていこうなという気持ちになりました。