はじめに
アドカレの14日目に紹介した記事の続編的な記事になります。
今回は F# + VSCode 環境でユニットテストを実施する方法を紹介したいと思います。
もし、まだ VSCode に F# の開発環境を導入していない方は、ぜひ こちら の記事を参考に導入してみてください!
開発
それでは実際に、テスト実施までの流れを追っていきたいと思います。
今回の記事も Powershell を利用して解説を進めさせていただきます。
ソリューションの作成
まずはソリューションを作成していきます。
以下のコマンドを実行することで、XunitPractice
というディレクトリの中に XunitPractice.sln
というソリューションファイルが作成されます。
# ソリューションファイルを作成する
mkdir XunitPractice | cd
dotnet new sln
プロジェクトの作成
今回は、テスト対象のプロジェクトを1つ
と xUnitプロジェクト1つ
用意することにします。
以下のコマンドを実行することで、XunitPracticeディレクトリ
以下にプロジェクトが2つ作成されます。
# テスト対象のプロジェクトを作成
dotnet new console -lang="F#" -o="SampleConsole"
# テストプロジェクトを作成
dotnet new xunit -lang="F#" -o="SampleConsoleTest"
各プロジェクトをソリューションに紐付ける
このままではソリューションに各プロジェクトが紐付いていないため、紐付ける作業が必要です。
以下のコマンドを実行することで、XunitPractice.sln
に SampleConsoleプロジェクト
と SampleConsoleTestプロジェクト
が登録されます。
# プロジェクトをソリューションに追加する
dotnet sln XunitPractice.sln add SampleConsole
dotnet sln XunitPractice.sln add SampleConsoleTest
プロジェクト同士を紐づける
また xUnitプロジェクト
はテスト対象のプロジェクトを参照に追加しなければならないので、以下のコマンドを実行して参照追加するようにします。
# テストプロジェクトにテスト対象プロジェクトを関連付ける
dotnet add SampleConsoleTest reference SampleConsole
ここまでくれば、ソリューションの準備は完了です。
次は VSCode の環境を整えつつ、実際に xUnit を利用していきます。
VSCode でビルド環境を整える
まずは以下のコマンドを実行して VSCode を起動します。
# VS Codeで開く
code .
そうしたら、F5
でビルド・実行ができる環境を整えます。
詳細な方法は こちら を参照してください。
以下の画像では、実際に修正した箇所に対して色付きの下線を引いています。
以上の修正ができたら、F5
実行で Hello World from F#!
が表示されることを確認します。
テスト対象のプロジェクトにモジュールを追加する
今回は SampleModule.fs
というファイルを追加することにします。
SampleModule.fs
の中身は f x
という関数があるだけの非常にシンプルなものです。
それでは、このモジュールを実際に使ってみましょう。
テストを実施してみる
それでは実際にテストを作成していきます。
SampleConsoleTestプロジェクト
の Tests.fs
を以下のように修正してみます。
module Tests
open Xunit
[<Fact>]
let ``SampleModule.f テスト`` () =
let 期待値 = 20
let 実績値 = SampleModule.f 10
Assert.Equal(実績値, 期待値)
インテリセンスが効くようになるまで結構な時間がかかる場合があるのでご注意ください。
インテリセンスが効くまでエディタ上でエラー扱いになってしまいますが、テストやビルドなどは通常通り動作するので問題ありません。
今回の SampleModule.f 関数
は、引数の値を2倍にする関数であるため、引数で渡した値の2倍の値が期待値となります。
期待値と実際の関数の処理結果(= 実績値)を比較したい場合には、Assert.Equal関数
を利用します。
Assert.Equal関数
は、引数で渡された 期待値 と 実績値 が一致している場合にテストをパスすることができます。
このようなテスト用の関数は他にも複数用意されています。それについては後述します。
上記の簡単なテストを作成したら、フラスコマークのメニューアイテムを選択します。
そうすると、テスト一覧が表示されているかと思います。
実行したいテストアイテムの右の方にある再生マーク(▶︎)を押下することで、テストを実行することが可能となります。
期待値 と 実績値 が想定通り一致していると、以下の画像のようにテスト結果がグリーンになります。
期待値 と 実績値 が一致しない場合、つまり想定した通りに関数が動いていない場合には以下のようにテスト結果がレッドとなります。
このようにxUnitを利用することで、作成したコードを簡単にテストすることができます。
テスト
ここからはテストで使えるいろいろなTIPSや機能について紹介していきます。
Assert
xUnitでは値のテストをするために Assertクラス のメソッドを利用します。
以下に、よく利用する検査用メソッドを紹介します。
時間があるときにすべて網羅したいと思います。
Equal (expected, actual)
Equal関数
は最も頻繁に使うAssertionの一つです。
- 第一引数に 期待値
- 第二引数に 実績値
を指定し、期待値と実績値が等しいこと を検査します。
期待値と実績値が等しい場合にテストをパスすることができます。
[<Fact>]
let ``Equal Test`` () =
let actual = 10 * 2
let expected = 20
Assert.Equal (expected, actual)
NotEqual (expected, actual)
- 第一引数に 期待値
- 第二引数に 実績値
を指定し、期待値と実績値が等しくないこと を検査します。
期待値と実績値が等しくない場合にテストをパスすることができます。
[<Fact>]
let ``Not Equal Test`` () =
let actual = 10 * 3
let expected = 20
Assert.NotEqual (expected, actual)
Throws<ExceptionType> (testCode)
- 型パラメータに 発生が期待される例外の型
- 第一引数に 検査するコード
を指定し、実際に 期待した例外が発生するか を検査します。
例外が発生した場合にテストをパスすることができます。
また、指定した例外の型の派生型がスローされた場合は、テストをパスできないので注意が必要です。
let func x = invalidArg "x" "invalid args"
[<Fact>]
let ``Throws Test`` () =
Assert.Throws<System.ArgumentException> (Action(func 10)) // OK
Assert.Throws<System.Exception> (Action(func 10)) // NG: 親クラスのExceptionを指定してもダメ
ThrowsAny<ExceptionType> (testCode)
- 型パラメータに 発生が期待される例外の型
- 第一引数に 検査するコード
を指定し、実際に 期待した例外が発生するか を検査します。
例外が発生した場合にテストをパスすることができます。
また、*Throws<ExceptionType> (testCode)
** の場合と異なり、指定した例外の型の派生型がスローされた場合にもテストがパスするようになります。
let func x = invalidArg "x" "invalid args"
[<Fact>]
let ``Throws Test`` () =
Assert.ThrowsAny<System.ArgumentException> (Action(func 10)) // OK
Assert.ThrowsAny<System.Exception> (Action(func 10)) // OK: 親クラスのExceptionを指定してもOK
Null (object)
- 第一引数に nullチェックしたいインスタンス
を指定し、nullであるか の検査をします。
インスタンスが null
の場合にテストをパスすることができます。
[<Fact>]
let ``Nulla Test`` () =
Assert.Null (null) // OK
Assert.Null (String.Empty) // NG
NotNull (object)
- 第一引数に non nullチェックしたいインスタンス
を指定し、nullでないか の検査をします。
インスタンスが null
ではない場合にテストをパスすることができます。
[<Fact>]
let ``Null Test`` () =
Assert.NotNull (null) // NG
Assert.NotNull (String.Empty) // OK
InRange (actual, low, high)
- 第一引数に 範囲チェックしたい値
- 第二引数に 範囲の下限値
- 第三引数に 範囲の上限値
を指定し、第一引数の値が 指定した範囲内に存在するか の検査をします。
low <= actual <= high
の範囲に値が存在している場合にテストをパスすることができます。
[<Fact>]
let ``InRange Test`` () =
let actual = 100
let lower = 0
let upper = 100
Assert.InRange (actual, lower, upper) // OK
let lower = 100
let upper = 200
Assert.InRange (actual, lower, upper) // OK
NotInRange (actual, low, high)
- 第一引数に 範囲チェックしたい値
- 第二引数に 範囲の下限値
- 第三引数に 範囲の上限値
を指定し、第一引数の値が 指定した範囲内に存在しないか の検査をします。
actual < low && high < actual
の範囲に値が存在している場合にテストをパスすることができます。
[<Fact>]
let ``NotInRange Test`` () =
let actual = 100
let lower = 0
let upper = 99
Assert.NotInRange (actual, lower, upper) // OK
let lower = 101
let upper = 200
Assert.NotInRange (actual, lower, upper) // OK
True (condition)
- 第一引数に Trueかどうかチェックしたい値
を指定し、第一引数の値が Trueかどうかチェック の検査をします。
condition
が true
の場合にテストをパスすることができます。
[<Fact>]
let ``True Test`` () =
Assert.True (0 = 0) // OK
Assert.True (0 = 1) // NG
False (condition)
- 第一引数に Falseかどうかチェックしたい値
を指定し、第一引数の値が Falseかどうかチェック の検査をします。
condition
が false
の場合にテストをパスすることができます。
[<Fact>]
let ``False Test`` () =
Assert.False(0 = 0) // NG
Assert.False(0 = 1) // OK
テストの種類
Fact
通常のテストです。
[<Fact>]
let ``テスト名`` () =
// テストコード
Theory
いろいろな値を使って検査をしたい場合に利用するテストです。
InlineData
一番お手軽な方法です。
InlineData属性
を利用することでテスト用のデータを引数に渡すことができます。
[<Theory>]
[<InlineData(引数リスト)>]
let ``テスト名`` パラメータリスト =
// テストコード
例えば引数を 3つ 受け取ってテストを行う場合、以下のようになります。
[<Theory>]
[<InlineData(1, 2, 3)>]
[<InlineData(2, 3, 5)>]
[<InlineData(3, 4, 7)>]
let ``足し算テスト`` (lhs:int) (rhs:int) (expected:int) =
let actual = lhs + rhs
Assert.Equal (expected, actual)
InlineData
を増やすことで検査項目を増やすことができます。
MemberData
テスト用のデータをプログラム的に生成したい場合に利用します。
InlineData
のときよりも複雑なデータを生成できる代わりに、クラスを作成しなければなりません。
type テストクラス名 () =
static member テスト用データ =
// テストデータ作成コード
[<Theory>]
[<MemberData("TestData")>]
member __.``テスト名`` パラメータリスト =
// テストコード
以下のように利用します。
type TestClass () =
static member TestData =
seq { 1..10 }
|> Seq.map (fun x -> [| x:>obj; (x*2):>obj |])
|> Seq.toArray
[<Theory>]
[<MemberData("TestData")>]
member __.``MemberData Test`` x expected =
let actual = x * 2
Assert.Equal (actual, expected)
ClassData
テストデータ作成方法が複雑になってくると、MemberData
では可読性が低下してきてしまい、どういったデータでテストをしたいのかがわかりにくくなってしまいます。
そういった場合に ClassData
を利用します。
type TestDataBase (xs:obj[] seq) =
interface seq<obj[]> with
member __.GetEnumerator () = xs.GetEnumerator()
member __.GetEnumerator () = xs.GetEnumerator() :> System.Collections.IEnumerator
type TestData () =
inherit TestDataBase ([[|1; 1;|];[|2; 3;|]])
[<Theory>]
[<ClassData(typeof<TestData>)>]
let ``ClassData Test`` s1 s2 =
Assert.Equal (s1, s2)
おわりに
以上が基本的なxUnitの使い方になります。
まだ他にもいろいろな機能がありますので、随時追記していきたいと思います。