みんな大好きテストの話です。
F#とテスト
F#ではC#のテストフレームワークがそのまま使えます。
NUnitやxUnit、MsTest等ですね。
これらC#用テストフレームワーク以外に、F#で書かれたF#向けのテストフレームワークもあります。
例えばFsUnitは上記C#用テストフレームワークのラッパーですが、よりF#っぽい文法でテストが書けるようにしてくれます。
今回はそれとは別の、expectoを使ってF#の単体テストを書いてみましょう。
expecto
expectoはF#で書かれた、F#用のテストフレームワークです(C#からも使えます。多分)。
"with APIs made for humans" とある通り、(機械ではなく)人間にとっての使いやすさを目指しているようです。
また、 "strong testing methodologies to everyone" とある通り、テストの並列・直列や同期・非同期のコントロール、fixtures、前処理と後処理のような補助を利用できる他、parametised testやproperty based testといったテスト手法をサポートしています。
その他、特徴として挙げられている点は「テストを単なる値として扱える」という部分です。
単なる値なので、テストそれ自体を代入したりリストにまとめたり、フィルタしたり組み合わせたりする事が容易です。
その他、負荷テスト用の機能やベンチマーカー等も付いていますが、今回は単体テストにだけ絞ります。
というわけで、使ってみましょう。
準備
NuGetで入ります。
テストを書く
サンプルとして載っている小さなテストを見てみましょう。
open Expecto
let tests =
test "A simple test" {
let subject = "Hello World"
Expect.equal subject "Hello World" "The strings should equal"
}
[<EntryPoint>]
let main args =
runTestsWithArgs defaultConfig args tests
test
まず、 tests
という変数をテストそのものに束縛しているのが見えます。これが、上記した「テストは単なる値」という事です。
テストですが、 test
関数を利用して生成しています。 test
関数は、文字列(テスト名)を取ってコンピュテーション式用のビルダークラスを返す関数です。コンピュテーション式の中で、Expectモジュールに定義されたテスト用関数を呼び出す事で、テストを組み立てる事ができます。
テスト用関数はテストする値と、テストに関するメッセージを引数に取ります。このメッセージはテストが失敗した場合に表示されます。
別にコンピュテーション式を使わずとも、単なる関数を使う事もできます。
open Expecto
let simpleTest =
testCase "A simple test" <| fun () ->
let expected = 4
Expect.equal expected (2+2) "2+2 = 4"
その場合は testCase
関数を使います。
実行
テストの実行部分を見てみましょう。
[<EntryPoint>]
let main args =
runTestsWithArgs defaultConfig args tests
メイン関数が定義されています。
expectoのテストは基本的に、NUnitやxUnitのように外部ツールにバイナリを見せるのではなく、テストコード自体を実行ファイルにコンパイルしてそれを実行する事で行います。
"No magic is involved here" という言の通り、テストも単なる関数の評価で行われます。runTestsWithArgsはテストコンフィグ、引数(並列・直列や表示形式の設定等)、そしてテストを取って、そのテストを実行し、結果をコンフィグや引数で指定された通りに出力する関数です。
このテストを msbuild Sample.fsproj && bin/Debug/Sample.exe
のようにコンパイルして実行すると、コンソールにテスト結果が出力されます。 --summary
オプションなどを付けると、個々のテストケースを表示してくれたりします。
応用的な使い方
テストの実行方法はわかりましたが、そのままでは少し使いづらいです。
テストを階層化してまとめたい場合、出力にその階層を表示して欲しいでしょう。
また、プロジェクト内にある全てのテストを変数に入れて管理するのは大変なので、他のテストフレームワークのように属性ベースでテストを指定したい事もあるでしょう。
どちらも可能です。
テストのグルーピング
これもサンプルコードです。
let tests =
testList "A test group" [
test "one test" {
Expect.equal (2+2) 4 "2+2"
}
test "another test that fails" {
Expect.equal (3+3) 5 "3+3"
}
testAsync "this is an async test" {
let! x = async { return 4 }
Expect.equal x (2+2) "2+2"
}
]
testList
関数を使う事で、テストを階層化・グループ化する事ができます。 testList
内に testList
をネストする事も可能です。
Tests属性
変数定義に属性を付ける事で、 runTestsInAssembly
を実行した時、自動的にそのコードをテストしてくれます。
(* テストされる *)
[<Tests>]
let tests =
test "A simple test" {
let subject = "Hello World"
Expect.equal subject "Hello World" "The strings should equal"
}
(* こっちはテストされない *)
let notTests =
test "Another test" {
Expect.equal 1 2 "failed!"
}
[<EntryPoint>]
let main args =
runTestsInAssembly defaultConfig args
その他、fixturesやpending tests, focusing testsのような便利な機能がありますので、READMEを読んでみてください。
property-based testing
関数型言語のテストと言えばproperty-based testingが有名です。
F#のproperty-based testing用フレームワークにはFsCheckがありますが、expectoはこれに対応しており、統合して使う事ができます。
READMEからサンプルを引きます。
// ExpectoFsCheckモジュールは自動でopenされる
// コンフィグ用のレコード定義もExpecto名前空間に入っている
open Expecto
let config = { FsCheckConfig.defaultConfig with maxTest = 10000 }
let properties =
testList "FsCheck samples" [
testProperty "Addition is commutative" <| fun a b ->
a + b = b + a
testProperty "Reverse of reverse of a list is the original list" <|
fun (xs:list<int>) -> List.rev (List.rev xs) = xs
// コンフィグを上書きする事も可能
testPropertyWithConfig config "Product is distributive over addition" <|
fun a b c ->
a * (b + c) = a * b + a * c
]
Tests.runTests defaultConfig properties
生成される値の範囲や、試行回数等の調整は、FsCheckのドキュメントも併せて参考にしてください。
ツールとの連携
テストフレームワークはそれ単体の性能だけでなく、IDEやCIサービスなどとの連携も重要です。expectoはどうでしょうか。
Visual Studio
expecto-adapterは、Visual Studio testのexpecto用アダプタで、これを入れると、Visual Studioのテスト機能でexpectoを利用できるようになります。
が、残念な事にまだ完璧ではないようで、時々テスト結果を解析できなかったりするようです。
AppVeyor
Windows環境でビルドやテストができる事で有名なCIサービスです。複数のテストフレームワークに対応していますが、残念ながらexpectoには対応していません。
「テストフレームワークに対応」というのは、結果をいい感じにパーズしてコンソールのTESTSタブに表示してくれる、という程度の意味です。
AppVeyorのテストコンテナ内では独自のスクリプトを走らせる事が可能なので、
after_build:
- ps: .\bin\Debug\Sample.exe --summary
test: off
のように設定すれば、expectoのテストをCIで実行する事は可能です。expectoはテストが失敗したら終了ステータスを1で終えるので、AppVeyorもこれを失敗と認識してくれて、きちんとテストする事は可能です。
その他のCIサービス、例えばtravis CI等でも、コマンドの終了ステータスが1の時はCI失敗扱いにしてくれるので、このあたりのCIサービスでも同じようにexpectoを使う事ができます。
その他
monoでも、.NET Coreでも使えるようです。
まとめ
それなりに使いやすい機能が揃っている、と感じるexpectoの紹介でした。
まだまだメジャーとは言えないフレームワークで、足りない機能もありますが、開発は活発で機能追加も続いているので、これからもっと使いやすくなっていくと思います。