他人様の知恵を拝借して、F#にモックフレームワークを導入しましたよー、というお話
私はC#ではMoqというモックフレームワークを愛用しているのですが、このMoq、F#においては思うように動作してくれなかったのです。
環境
- Visual Studio 2010 Professional
- NUnit 2.5.7
- Moq 3.1
- Moq 4.0 Beta
- Rhino Mocks 3.6
を利用しております。その他の環境では確認しておりませんのでご了承下さい。
Moq
C#では使い勝手の良いMoqを何とか、F#でも使おうとあーだこーだソースをこねくり回していましたが、結局挫折しました。
F#のラムダ式をLINQ expressionsに変換する必要があるのですが、その変換にどうやら無理があるようです。
ググったら以下のようなエントリを見つけました。
- http://bloggingabout.net/blogs/vagif/archive/2010/08/04/mock-framework-challenges-in-f.aspx
- F#でもMoq使いたかったんだけど、なんかダメそうなのでRhino Mocksを採用しました。 - Bug Catharsis
ふむふむ。どちらもMoqはF#では今ひとつだよと書かれてますね。
それで、F#でモックフレームワークを使うならRhino Mocksが良いとも書かれてます。
Rhino Mocksとはなんぞや?
Rhino Mocks
先のブログによりますと、"Rhino.Mocks is the most popular .NET mock framework."だそうです。
私、今まで知りませんでした。モックフレームワークって多彩なんですね。
という訳でRhino Mocksを試してみました。
以下のインタフェースに対してモックを作ってみたいと思います。
type IGreet = interface abstract Greet : string -> string end
例によって、Rhino MocksのDLLを参照に追加して、テストコードは以下のようになります。
open System open NUnit.Framework open Rhino.Mocks [<TestFixture>] type MyTest() = [<Test>] member x.RhinoMockSample () = let mock1 = MockRepository.GenerateMock<IGreet>() RhinoMocksExtensions.Stub<IGreet, string>(mock1, fun m -> m.Greet "hello").Return("Hello") |> ignore Assert.AreEqual("Hello", mock1.Greet "hello") RhinoMocksExtensions.VerifyAllExpectations(mock1) let mock2 = MockRepository.GenerateMock<IGreet>() RhinoMocksExtensions.Stub<IGreet, string>(mock2, fun m -> m.Greet "hello2").Return("HelloHello") |> ignore Assert.AreEqual("HelloHello", mock2.Greet "hello2") RhinoMocksExtensions.VerifyAllExpectations(mock2) let mock3 = MockRepository.GenerateMock<IGreet>() RhinoMocksExtensions.Stub<IGreet, string>(mock3, fun m -> m.Greet "hello3").Return("HelloHelloHello") |> ignore Assert.AreEqual(null, mock3.Greet "hell") RhinoMocksExtensions.VerifyAllExpectations(mock3)
ビルドし、テストしたところ、無事にテストにパスしました。
さらに、先のブログにこんな便利なラッパーがあったのでご紹介します。
(* original-> http://bloggingabout.net/blogs/vagif/archive/2010/08/04/mock-framework-challenges-in-f.aspx *) type mock<'T when 'T : not struct>() = let instance = MockRepository.GenerateMock<'T>() member this.Object = instance module Mock = let arrange<'T, 'R when 'T : not struct>(f : ('T -> 'R))(r : 'R)(m : mock<'T>) = RhinoMocksExtensions.Stub<'T, 'R>(m.Object, Function<'T, 'R>(f)).Return(r)
これを使うとRhino Mocksがさらに簡単に書けるようになります。
[<Test>] member x.MockTest2 () = let mock1 = mock<IGreet>() mock1 |> Mock.arrange (fun m -> m.Greet "hello") "HELLO" |> ignore let greet = mock1.Object Assert.AreEqual("HELLO", greet.Greet "hello") Assert.AreEqual(null, greet.Greet "hell") RhinoMocksExtensions.VerifyAllExpectations(greet)
とても綺麗ですね。
以下は、少し自分好みに改良してみたつもりです。
type mock<'T when 'T : not struct>() = let instance = MockRepository.GenerateMock<'T>() member this.Object = instance member this.Setup<'R when 'T : not struct>(f : ('T -> 'R))(r : 'R) = RhinoMocksExtensions.Stub<'T, 'R>(this.Object, Function<'T, 'R>(f)).Return(r) member this.Setup2<'R when 'T : not struct>(f : ('T -> 'R)) = RhinoMocksExtensions.Stub<'T, 'R>(this.Object, Function<'T, 'R>(f))
文脈を解り易くしたいと思い、Setup2はReturnを省略してみました。
テストコードは以下のようになります。
[<Test>] member x.MockTest2 () = let mock1 = mock<IGreet>() mock1.Setup (fun m -> m.Greet "hello") "HELLO" |> ignore (mock1.Setup2 (fun m -> m.Greet "日本語")).Return "こんにちは" |> ignore let greet = mock1.Object Assert.AreEqual("HELLO", greet.Greet "hello") Assert.AreEqual("こんにちは", greet.Greet "日本語") Assert.AreEqual(null, greet.Greet "hell") RhinoMocksExtensions.VerifyAllExpectations(greet)
…。
Setup2は余計でしたね。
こんな感じで、Rhino MocksはF#ではかなり使える存在になりそうです。