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#の型推論が如何に強力か垣間見ることができますね。

テスト大事

2回に渡って、F#でNUnitテストコードを記述する方法を紹介しました。
手探りで調べているため冗長な点があったかもしれませんが、参考になれば幸いです。
また、間違いがあればコメント欄やTwitterで教えてください。