F# でEither型のテストを書く

F#でEither型を利用することはよくあることでしょう。特に例外処理関連で。

type Either<'T, 'U> =
  | Left of 'T
  | Right of 'U

さて、このEitherは色々な状況で使用できるのですが、F#でもっとも利用頻度が高い場面は例外処理のとき。

let divide x = function
| y when y = 0 -> Left (ArgumentException "ばーん!")
| y -> Right (x / y)

さて、これで処理が完了したのか例外が発生したのかが型で判断できる。
というわけでテストを書きましょう。

[<Scenario>]
let ``割る数が0でなければRightに包まれた除算結果が返る`` () =
  Given (1, 1)
  ||> When divide
  |> It should equal (Right 1)
  |> Verify

正しい値かどうかのテストはこれでOK。では例外が発生する場合のテストは?
ということで単純に書こうとしても、以下の書き方では失敗する。

(* これだと失敗する *)
[<Scenario>]
let ``割る数が0であればLeftに包まれた例外が返る`` () =
  Given (1, 0)
  ||> When divide
  |> It should equal (Left (ArgumentException "ばーん!"))
  |> Verify

ArgumentExceptionが.NETなクラスなのでそりゃそうですね。
どうしよう…とtwitterに投げてみたところ。

なるほどなるほど、早速試してみましょう。
ということで、試行錯誤の後にこんなコードを書いてみました。

open NUnit.Framework

let message : (obj -> string) = function
| :? string as x -> x
| :? exn as x -> x.Message
| x -> x.ToString()

let letfValue<'x> (expectedMessage:string) (either:Either<'x,_>) =
  printMethod expectedMessage
  match either with
  | Left x -> Assert.AreEqual(expectedMessage, (x |> message)); true
  | Right _ -> false

ジェネリック型を指定しつつ、メッセージが期待するものと一致するか比較。
Left x のxがException以外の型な場合を想定して、ToStringで比較できるようにもしてみた。
それでは、はりきってテスト。

[<Scenario>]
let ``割る数が0であればLeftに包まれた例外が返る`` () =
  Given (1, 0)
  ||> When divide
  |> It should have (letfValue<ArgumentException> "えらーしか!")
  |> Verify

というわけで、Eitherを使ったときの例外の型とメッセージのテストをやってみました。