F#でNUnitテストコードを記述する方法2
前回は、NUnitを用いてsquareメソッドのテストを一つ作成しました。
module TestingNamespace.TestingModule open System open NUnit.Framework open TestedNamespace.TestedModule [<TestFixture>] type MyTest() = [<Test;Description("example description")>] member x.TestSquare1 () = Assert.AreEqual (4, square 2)
さて、どんどん追加していきましょう。
NUnit2.5で追加されたTestCase属性の利用です。
[<TestCase(0, Result=0)>] [<TestCase(1, Result=1)>] [<TestCase(2, Result=4)>] [<TestCase(100, Result=10000)>] [<TestCase(-1, Result=1)>] [<TestCase(-17, Result=289)>] member x.TestSquare2 (input) = square input
これは下記でも同じです。
[<TestCase(0, 0)>] [<TestCase(1, 1)>] [<TestCase(2, 4)>] [<TestCase(100, 10000)>] [<TestCase(-1, 1)>] [<TestCase(-17, 289)>] //member x.TestExample3 (input, result) = //カリー化形式でも書けます。 member x.TestSquare3 input result = Assert.That (square input, Is.EqualTo(result), sprintf "%d ** 2 = %d" input result)
メッセージをsprintfメソッドで記述できるのは良いですね。
ですけど、毎回TestCase属性を追加するのは迂遠ですよね。何だか見づらいですし、
TestCaseSourceによるテストデータの記述は以下のように書けばOKです。
//TestData1 : int [][] static member TestData1 = [| [| 0; 0 |]; [| 1; 1 |]; [| 2; 4 |]; [| 100; 10000 |]; [| -1; 1 |]; [| -17; 289 |]; |] [<TestCaseSource("TestData1")>] member x.TestSquare4 input result = Assert.That (square input, Is.EqualTo(result), sprintf "%d ** 2 = %d" input result)
ポイントとしては、TestData1は『「int型の配列」の配列』になっている点ですね。
を見てもそのように見て取れると思います。
qsortメソッドのテスト
次は前回空気だったqsortメソッドのテストです。
qsortメソッドは 'a list(アルファ・リストと読むらしいです)を引数にとり、'a list を返すメソッドです。
素直に書くとこうなります。
[<Test>] member x.TestSort1 () = Assert.AreEqual ([1; 2; 3] , qsort [3; 2; 1]) Assert.AreEqual ([1.5; 2.5; 3.5] , qsort [3.5; 2.5; 1.5]) Assert.AreEqual (["alpha"; "beta"; "gamma"], qsort ["beta"; "gamma"; "alpha"])
ふむふむ。上記と同じようにTestCaseSource属性を用いて以下のように書きたくなります。
//このコードは問題があります。 static member TestDataE2 = [| [| []; [] |]; [| [2]; [2] |]; [| [2; 1]; [1; 2] |]; [| [5;7;2;9;3;4;5;1;0;4]; [0;1;2;3;4;4;5;5;7;9] |]; |] [<TestCaseSource("TestDataE2")>] member x.TestSortE2 input result = Assert.AreEqual ( result, qsort input, sprintf "qsort %A -> %A" input result)
実行していただくと解りますが、このコードはビルドを通過します。
しかしNUnitで実行時に下記のようなエラーが出てテストに失敗します
- Unable to determine type arguments for fixture
どうやら型情報を与えてあげなければならないようです。
テストをパスするコードは以下になります。
static member TestData2 : System.IComparable list [][] = [| [| []; [] |]; [| [2]; [2] |]; [| [2; 1]; [1; 2] |]; [| [5;7;2;9;3;4;5;1;0;4]; [0;1;2;3;4;4;5;5;7;9] |]; [| [100.0; 2.5; 1.5; 0.1]; [0.1; 1.5; 2.5; 100.0]|]; [| [ 'b';'e';'g';'f';'c';'h';'a';'d']; ['a';'b';'c';'d';'e';'f';'g';'h'] |] [| ["beta"; "gamma"; "delta"; "alpha"]; ["alpha"; "beta"; "delta"; "gamma"] |]; |] [<TestCaseSource("TestData2")>] member x.TestSort2 (input : System.IComparable list) result = Assert.AreEqual ( result, qsort input, sprintf "qsort %A -> %A" input result)
TestData2は【『「System.IComparableのlist」の配列』の配列】だと型を指定します。
そしてテストメソッドの引数inputも「System.IComparableのlist」だと明記します。
これでOKです。
嬉しいことに、「System.IComparableのlist」である「floatのlist」や「stringのlist」もテスト出来ます。
qsort : 'a list -> 'a list ですから、このようなテストコードが書けるのは喜ばしいことですね。
この例で、Object listを指定するとビルドエラーが発生します。
squareFメソッドのテスト
次に、これまた前回出番のなかったsquareFメソッドのテストです。
squareFメソッドは、int引数の自乗をfloatで返す実用性皆無のメソッドです。
TestCaseSourceで与える配列は『「inputとresultの配列」の配列』です。
squareメソッドやqsortメソッドは引数と返値の型が一致していましたが、squareFメソッドはint -> floatです。
よって、今度こそObject型の出番です。
static member TestData3 : Object [][] = [| [| 1; 1.0 |]; [| 2; 4.0 |]; [| 100; 10000.0 |]; [| -1; 1.0 |]; [| -17; 289.0 |]; |] [<TestCaseSource("TestData3")>] member x.TestSquareF1 input result = Assert.That (squareF input, Is.EqualTo(result), sprintf "%d ** 2 = %f" input result)
このテストコードはビルドを通過しテストをパスします。
一見問題無いように見えます。
しかし、TestData3はObjectであるため、例えば以下のコードはビルドを通過します。
//このコードは問題があります。 static member TestData3E : Object [][] = [| [| 1; 1.0 |]; [| 'c'; "cc"|]; //ビルドエラーにならないが、テストに失敗する。 [| 4.0; 16.0|] |] [<TestCaseSource("TestData3E")>] member x.TestSquareF1E input result = Assert.That (squareF input, Is.EqualTo(result), sprintf "%d ** 2 = %f" input result)
上記のコードはもちろん実行時(テスト時)に失敗します。
- System.ArgumentException : 型 'System.String' のオブジェクトを型 'System.Double' に変換できません。
- System.ArgumentException : 型 'System.Double' のオブジェクトを型 'System.Int32' に変換できません。
ですが、TestData3EとTestSquareF1Eの関係までは判断されないため、ビルドエラーにはなりません。
「テストのポリシーとして予期せぬ入力もテストする」という考え方なら、上記もアリかもしれませんが、
F#の強力な型推論と型付システムをテストにも活かしたいです。
結論から言うと、タプルを利用することで解決しました。
//TestData4 : (int * float) [][] static member TestData4 = [| [| (1, 1.0) |]; [| (2, 4.0) |]; [| 5, 25.0 |]; //括弧は省略できる。 //[| ('c', "cc") |]; //ビルドエラーになる。 //[| (4.0, 16.0) |] |] [<TestCaseSource("TestData4")>] //sprintf文を書いている場合はresultの型が推定されるため、下記でもテストに通過する。 //member x.TestSquareF2 data = member x.TestSquareF2 (data : int * float) = let input, result = data in Assert.That (squareF input, Is.EqualTo(result), sprintf "%d ** 2 = %f" input result)
TestData4を『「(intとfloatのタプル)を一つだけ持つ配列」の配列』としています。
TestSquareF2の最初でタプルをinputとresultに分解しています。
またTestSquareF2の引数 dataの型を明記していますが、これはメッセージのsprintf "%d ** 2 = %f" input result文を書いている場合は、resultの型がfloatと推定されるため、
member x.TestSquareF2 data =
だけでOKとなります。F#の型推論が如何に強力か垣間見ることができますね。