F# 向けモックライブラリPersimmockを試作

モックフレームワークやモックライブラリは.NET界隈にいくつかあって、特にF#にはFoqがあるわけですが、現状で満足か、と問われると微妙な顔持ちになります。

以下、1年半くらい前にFoqの行けてないところをzeclさんに尋ねたときの会話です。

実際、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 + オブジェクト式でやっていこうなという気持ちになりました。