テスト駆動開発

プログラミングの方法論の一つとして テスト駆動開発という手法があります。 これはとても変わった開発手法で 「プログラム本体よりもテストから作る」 という開発方式です。 テスト駆動開発を含む、eXtreme Programmingという手法の提唱者である Kent beckによるテスト駆動開発入門という本が詳しいです。 また、世の中にはテスト駆動開発をサポートする 通称、Xunitと呼ばれる単体テスト支援ツールがあります。  JavaならJUnit  C++ならcppunit  全.Net言語対象のNUnit  他にも有名な言語には大抵なんちゃらunitがあります  Xunit←かのMartin Fowler氏による 特に、JUnit(1997年生まれ)が テスト駆動開発の火付け役になったと言えると思います。 元々このサイトでは.Net版のNUnitの解説を予定してたのですが F#版であるFsUnitというのがあることを知ったため こちらについて調べてみました。 なお、NUnitの話は、Expert F#の(p537-542)と Foundations of F#の(p311-312)に載ってますのでご参考までに

FsUnit

FsUnitはF#の単体テストをよりenjoyableにするためのライブラリの集合です。 最初はFsUnit単体で存在していましたがNUnitのラッパーのような位置づけになり 現在(バージョン5.0.0時点)ではNUnit,xUnit,MsTestの3つの単体テストフレームワークに対応しています。 ここでは、FsUnit単体で使用する方法をまず述べ、 次にテストプロジェクトをソリューションに追加してテストエクスプローラーでテストが実行できるようにするまでを解説します。 まずは、FsUnitのインストール法について説明します。 最初にNuGetというパッケージ管理ツールが必要になりますが 本サイトの導入編のF#のインストールのページの方法で Visual Studio 2022 コミュニティエディションをインストールした方は すでにVisual StudioにセットでNuGetもインストールされています。 いつものようにコンソールプロジェクトを開き、画面の上部から ツール(T) -> NuGetパッケージマネージャー(N) -> パッケージマネージャーコンソール(O) と進んでください。 デフォルトでは画面下部にパッケージマネージャーコンソールが開くと思います。 そこで次のコマンドを打ってください。
NuGetでFsUnitをインストールするコマンド
PM> Install-Package FsUnit -Version 5.0.0
これで、NUnitなどFsUnitに必要なものも含めてまとめてインストールされます。 インストールすると.fsprojに <ItemGroup> <PackageReference Include="FsUnit" Version="5.0.0" /> </ItemGroup> のようなFsUnitの参照をする個所が追加されると思います。 この状態でFsUnitが動作することを確かめてみましょう。 次のようなコードを打ってみてください。特に例外も何も発生せずに終了していればOKです。 コメントアウトした個所のコメントを外せば例外が発生します。 これはこちらのページに載っているコードをまとめたものです。 個別に解説はしませんが、英語として読めるようになっているので大体理解できると思います。詳しくは上のサイトを参照ください。
FsUnitのサンプルコード
open FsUnit

1 |> should equal 1
1 |> should not' (equal 2)
[2;4;6] |> should equivalent [4;6;2]
[2;4;6] |> should not' (equivalent [4;8;2])
10.1 |> should (equalWithin 0.1) 10.11
10.1 |> should not' ((equalWithin 0.001) 10.11)
"ships" |> should startWith "sh"
"ships" |> should not' (startWith "ss")
"ships" |> should endWith "ps"
"ships" |> should not' (endWith "ss")
"ships" |> should haveSubstring "hip"
"ships" |> should not' (haveSubstring "pip")
[1] |> should contain 1
[] |> should not' (contain 1)
(fun () -> failwith "BOOM!" |> ignore) |> should throw typeof<System.Exception>
(fun () -> failwith "BOOM!" |> ignore) |> should (throwWithMessage "BOOM!") typeof<System.Exception>
//shouldFail (fun () -> 5/0 |> ignore)
true |> should be True
false |> should not' (be True)
"" |> should be EmptyString
"" |> should be NullOrEmptyString
null |> should be NullOrEmptyString
null |> should be Null
null |> should be null
11 |> should be (greaterThan 10)
9 |> should not' (be greaterThan 10)
11 |> should be (greaterThanOrEqualTo 10)
9 |> should not' (be greaterThanOrEqualTo 10)
10 |> should be (lessThan 11)
10 |> should not' (be lessThan 9)
10.0 |> should be (lessThanOrEqualTo 10.1)
10 |> should not' (be lessThanOrEqualTo 9)
0.0 |> should be ofExactType<float>
1 |> should not' (be ofExactType<obj>)
[] |> should be Empty
[1] |> should not' (be Empty)
"test" |> should be instanceOfType<string>
"test" |> should not' (be instanceOfType<int>)
2.0 |> should not' (be NaN)
[1;2;3] |> should be unique
[1;2;3] |> should be ascending
[1;3;2] |> should not' (be ascending)
[3;2;1] |> should be descending
[3;1;2] |> should not' (be descending)

[1..10] |> should be (supersetOf [3;6;9])
[1..10] |> should not' (be supersetOf [5;11;21])

[3;6;9] |> should be (subsetOf [1..10])
[5;11;21] |> should not' (be subsetOf [1..10])
type TestUnion = First | Second of int | Third of string
First |> should be (ofCase<@ First @>)
First |> should be (ofCase<@ First, Second @>) // checks if on the cases matches the given case
Second 5 |> should be (ofCase<@ Second 10 @>) // note, the actual value is not checked!
First |> should not' (be ofCase<@ Second 5 @>)
//5 |> should be (ofCase<@ Second 5 @>) // will throw an exception
//Second 5 |> should be (ofCase<@ int @>) // will throw an exception
これで準備は整いました。 このままでもFsUnitは使えますが 次はテストプロジェクトをソリューションに追加しテストエクスプローラーで テストが実行され、すべて緑色(テストをパスした)になるのを視覚的に見えるようにしていきます。 今、コンソールアプリとしてプロジェクトを作っていると思いますので ファイル(F) -> 新規作成(N) -> プロジェクト(P) と進み上部の検索窓にNUnitと打ち込んでEnterをおし NUnit テスト プロジェクト(3つぐらい表示されますがF#と書いてあるものを選びます)を選択してください 次に進み、プロジェクト名(なんとかTestなどと名づけるといいと思います。ここではCon1Testとしました)をいれ ソリューションの個所はデフォルトの新しいソリューションを作成ではなくソリューションに追加を選んでください。 次へ進みフレームワークはそのまま(.NET 6.0 長期的なサポート)で作成を押してください これで単体テスト用のプロジェクトが作成されました このプロジェクトにはFsUnitが含まれていないので 右側のソリューションエクスプローラーでCon1Test(あなたがつけたプロジェクト名)をダブルクリックし ItemGroupの中にFsUnitの行を追加してください 私の環境では次のようになっています。 <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="coverlet.collector" Version="3.1.0" /> <PackageReference Include="FsUnit" Version="5.0.0" /> </ItemGroup> 私の環境では、実行するとNUnitのバージョンが3.13.2になっているという警告が出たので NUnitのバージョンを手動で3.13.3にするとエラーがでなくなりました。 これでようやくテストを書く準備が整いました。 テストプロジェクトの方にUnitTest1.fsがあると思いますので そこに次のようなコードを書きます。 [<Test>]をつけたものがテストケースになります。
NUnitとFsUnitを複合して使う
module Con1Test
open FsUnit
open NUnit.Framework


[<Test>]
let nunitTest () =
    Assert.AreEqual(2.0**10.0, 1024.0)

[<Test>]
let fsunitTest() =
    2.0**10.0 |> should equal 1024.0

// 識別子名自体をテストの説明にしてしまうのもあり
[<Test>]
let ``the value of 2 raised to 10th power is 1024`` () =
    2.0**10.0 |> should equal 1024.0
ここでは3つのテストを書きました。 1つ目はFsUnitではなくNUnitのテストコードでAssert.AreEqualを使っています。 2つ目はFsUnitのテストコードです。 3つ目は``をつけて識別子名をつけることで空白を含んだ識別子名をつけることができるのですが それを利用してテストケースの説明を識別子名としています。 この状態でテスト(S)のすべてのテストを実行(R)とすると 3つのテストが実行され、問題なければ緑色で表示がなされます。 試しにテストのうちの一つを1023.0などに変えて実行するとエラーとなり 赤く表示されます なお、3つ目の例では日本語文字列も使えるのですが テストエクスプローラー(テストの実行結果を表示する画面)では文字化けしてしまいます。 なのでテストケースは英語かローマ字で説明するのが良いでしょう。 次に、テストケースを複数入力する方法を解説します。
複数のテストケースを入力
module Con1Test
open FsUnit
open NUnit.Framework


[<TestCase("",      0)>]
[<TestCase("ab",    2)>]
[<TestCase("def",   3)>]
[<TestCase("GHIJ",  4)>]
[<TestCase("56789", 5)>]
let testcasetestsample(str: string, expected: int) =
    str.Length |> should equal expected

let testcases = ["ab";"cd";"xy";"Az";"TO"]
[<TestCaseSource("testcases")>]
let length2strings(str: string) =
    str.Length |> should equal 2
このプログラムでは、[<TestCase()>]を使って直接テストケースを与える方法と [<TestCaseSource()>]を使ってテストケースを外部から与える方法を示しています。 どちらも、与えたテストケースの数だけテストが追加されます。 最後に、クラスをテストする方法を示します。 クラスをテストする場合、[<TestFixture>]属性をクラスに与える必要があります。 具体的には次のようになります。
クラスのテスト
module Con1Test
open FsUnit
open NUnit.Framework

[<TestFixture>]
type ClassTest() =
  [<Test>]
  member this.easyTest() = 
    1 |> should equal 1
  [<Test>]
  member this.``1 should equal 1 testt`` () = 
    1 |> should equal 1
以上です。快適な単体テストライフを送ってください。