F#でxUnitを使う方法

# はじめに

[アドカレの14日目に紹介した記事](https://qiita.com/Midoliy/items/1aee06ada5c0081c49d1)の続編的な記事になります。
今回は F# + VSCode 環境でユニットテストを実施する方法を紹介したいと思います。

もし、まだ VSCode に F# の開発環境を導入していない方は、ぜひ [**こちら**](https://qiita.com/Midoliy/items/1aee06ada5c0081c49d1) の記事を参考に導入してみてください！

# 開発

それでは実際に、テスト実施までの流れを追っていきたいと思います。
今回の記事も **Powershell** を利用して解説を進めさせていただきます。

## ソリューションの作成

まずはソリューションを作成していきます。
以下のコマンドを実行することで、`XunitPractice` というディレクトリの中に `XunitPractice.sln` というソリューションファイルが作成されます。

```powershell
# ソリューションファイルを作成する
mkdir XunitPractice | cd
dotnet new sln
```

## プロジェクトの作成

今回は、`テスト対象のプロジェクトを1つ` と `xUnitプロジェクト1つ` 用意することにします。
以下のコマンドを実行することで、`XunitPracticeディレクトリ` 以下にプロジェクトが2つ作成されます。

```powershell
# テスト対象のプロジェクトを作成
dotnet new console -lang="F#" -o="SampleConsole"
# テストプロジェクトを作成
dotnet new xunit -lang="F#" -o="SampleConsoleTest"
```

## 各プロジェクトをソリューションに紐付ける

このままではソリューションに各プロジェクトが紐付いていないため、紐付ける作業が必要です。
以下のコマンドを実行することで、`XunitPractice.sln` に `SampleConsoleプロジェクト` と `SampleConsoleTestプロジェクト` が登録されます。

```powershell
# プロジェクトをソリューションに追加する
dotnet sln XunitPractice.sln add SampleConsole
dotnet sln XunitPractice.sln add SampleConsoleTest
```

## プロジェクト同士を紐づける

また `xUnitプロジェクト` はテスト対象のプロジェクトを参照に追加しなければならないので、以下のコマンドを実行して参照追加するようにします。

```powershell
# テストプロジェクトにテスト対象プロジェクトを関連付ける
dotnet add SampleConsoleTest reference SampleConsole
```

ここまでくれば、ソリューションの準備は完了です。
次は **VSCode** の環境を整えつつ、実際に **xUnit** を利用していきます。

## VSCode でビルド環境を整える

まずは以下のコマンドを実行して **VSCode** を起動します。

```powershell
# VS Codeで開く
code .
```

そうしたら、`F5` でビルド・実行ができる環境を整えます。
詳細な方法は [**こちら**](https://qiita.com/Midoliy/items/1aee06ada5c0081c49d1) を参照してください。

以下の画像では、実際に修正した箇所に対して色付きの下線を引いています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/fb78e968-5a9a-ee4c-6f75-cd2f23925166.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/36957353-5897-426f-0609-9b243912967e.png)

以上の修正ができたら、`F5` 実行で `Hello World from F#!` が表示されることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/dc2c15dd-d74f-09ed-9b2b-a08ec54c0fcb.png)

## テスト対象のプロジェクトにモジュールを追加する

今回は `SampleModule.fs` というファイルを追加することにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/5013708b-13e2-647a-e197-805036c54e0d.png)

`SampleModule.fs` の中身は **`f x`** という関数があるだけの非常にシンプルなものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/7c78463d-9c42-1bc2-e536-fd81e62c8f29.png)

それでは、このモジュールを実際に使ってみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/0b80bee4-1aa8-9fbe-02a6-454a16a12fa5.png)

## テストを実施してみる

それでは実際にテストを作成していきます。

`SampleConsoleTestプロジェクト` の `Tests.fs` を以下のように修正してみます。

```fsharp
module Tests

open Xunit

[<Fact>]
let ``SampleModule.f テスト`` () =
    let 期待値 = 20
    let 実績値 = SampleModule.f 10
    Assert.Equal(実績値, 期待値)
```

> インテリセンスが効くようになるまで結構な時間がかかる場合があるのでご注意ください。
> インテリセンスが効くまでエディタ上でエラー扱いになってしまいますが、テストやビルドなどは通常通り動作するので問題ありません。

今回の `SampleModule.f 関数` は、引数の値を2倍にする関数であるため、引数で渡した値の2倍の値が期待値となります。
期待値と実際の関数の処理結果(= 実績値)を比較したい場合には、`Assert.Equal関数` を利用します。

`Assert.Equal関数` は、引数で渡された **期待値** と **実績値** が一致している場合にテストをパスすることができます。
このようなテスト用の関数は他にも複数用意されています。それについては後述します。

上記の簡単なテストを作成したら、フラスコマークのメニューアイテムを選択します。
そうすると、テスト一覧が表示されているかと思います。

<img width="1235" alt="スクリーンショット 2019-12-21 17.20.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/a1e57d5a-998c-37da-6a74-f6a0e3930d10.png">

実行したいテストアイテムの右の方にある再生マーク(▶︎)を押下することで、テストを実行することが可能となります。

<img width="1028" alt="スクリーンショット 2019-12-21 17.24.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/27b7fb60-e4c0-244f-7d2d-46311bf7d149.png">

**期待値** と **実績値** が想定通り一致していると、以下の画像のようにテスト結果がグリーンになります。

<img width="361" alt="スクリーンショット 2019-12-21 17.26.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/8b012d70-14c4-2e9d-e2bf-e9a36a52d4a7.png">

**期待値** と **実績値** が一致しない場合、つまり想定した通りに関数が動いていない場合には以下のようにテスト結果がレッドとなります。

<img width="357" alt="スクリーンショット 2019-12-21 17.28.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/2a0a2d2d-3a0f-6862-c044-67b1eedce35a.png">

このようにxUnitを利用することで、作成したコードを簡単にテストすることができます。

# テスト

ここからはテストで使えるいろいろなTIPSや機能について紹介していきます。

## Assert

xUnitでは値のテストをするために **Assertクラス** のメソッドを利用します。
以下に、よく利用する検査用メソッドを紹介します。

> 時間があるときにすべて網羅したいと思います。

### Equal (expected, actual)

`Equal関数` は最も頻繁に使うAssertionの一つです。

- 第一引数に **期待値**
- 第二引数に **実績値**

を指定し、**期待値と実績値が等しいこと** を検査します。
期待値と実績値が等しい場合にテストをパスすることができます。

```fsharp
[<Fact>]
let ``Equal Test`` () =
    let actual = 10 * 2
    let expected = 20
    Assert.Equal (expected, actual)
```


### NotEqual (expected, actual)

- 第一引数に **期待値**
- 第二引数に **実績値**

を指定し、**期待値と実績値が等しくないこと** を検査します。
期待値と実績値が等しくない場合にテストをパスすることができます。

```fsharp
[<Fact>]
let ``Not Equal Test`` () =
    let actual = 10 * 3
    let expected = 20
    Assert.NotEqual (expected, actual)
```

### Throws&lt;ExceptionType&gt; (testCode)

- 型パラメータに **発生が期待される例外の型**
- 第一引数に **検査するコード**

を指定し、実際に **期待した例外が発生するか** を検査します。
例外が発生した場合にテストをパスすることができます。

また、指定した例外の型の派生型がスローされた場合は、テストをパスできないので注意が必要です。

```fsharp
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&lt;ExceptionType&gt; (testCode)

- 型パラメータに **発生が期待される例外の型**
- 第一引数に **検査するコード**

を指定し、実際に **期待した例外が発生するか** を検査します。
例外が発生した場合にテストをパスすることができます。

また、*Throws&lt;ExceptionType&gt; (testCode)
** の場合と異なり、指定した例外の型の派生型がスローされた場合にもテストがパスするようになります。

```fsharp
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` の場合にテストをパスすることができます。

```fsharp
[<Fact>]
let ``Nulla Test`` () =
    Assert.Null (null)              // OK
    Assert.Null (String.Empty)      // NG
```

### NotNull (object)

- 第一引数に **non nullチェックしたいインスタンス**

を指定し、**nullでないか** の検査をします。
インスタンスが `null` ではない場合にテストをパスすることができます。

```fsharp
[<Fact>]
let ``Null Test`` () =
    Assert.NotNull (null)              // NG
    Assert.NotNull (String.Empty)      // OK
```

### InRange (actual, low, high)

- 第一引数に **範囲チェックしたい値**
- 第二引数に **範囲の下限値**
- 第三引数に **範囲の上限値**

を指定し、第一引数の値が **指定した範囲内に存在するか** の検査をします。
**`low <= actual <= high`** の範囲に値が存在している場合にテストをパスすることができます。

```fsharp
[<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`** の範囲に値が存在している場合にテストをパスすることができます。

```fsharp
[<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` の場合にテストをパスすることができます。

```fsharp
[<Fact>]
let ``True Test`` () =
    Assert.True (0 = 0)     // OK
    Assert.True (0 = 1)     // NG
```

### False (condition)

- 第一引数に **Falseかどうかチェックしたい値**

を指定し、第一引数の値が **Falseかどうかチェック** の検査をします。
`condition` が `false` の場合にテストをパスすることができます。

```fsharp
[<Fact>]
let ``False Test`` () =
    Assert.False(0 = 0)     // NG
    Assert.False(0 = 1)     // OK
```

## テストの種類

### Fact

通常のテストです。

```fsharp
[<Fact>]
let ``テスト名`` () =
    // テストコード
```

### Theory

いろいろな値を使って検査をしたい場合に利用するテストです。

#### InlineData

一番お手軽な方法です。
**`InlineData属性`** を利用することでテスト用のデータを引数に渡すことができます。

```fsharp
[<Theory>]
[<InlineData(引数リスト)>]
let ``テスト名`` パラメータリスト =
    // テストコード
```

例えば引数を 3つ 受け取ってテストを行う場合、以下のようになります。

```fsharp
[<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`** のときよりも複雑なデータを生成できる代わりに、クラスを作成しなければなりません。

```fsharp
type テストクラス名 () =
    static member テスト用データ =
        // テストデータ作成コード

    [<Theory>]
    [<MemberData("TestData")>]
    member __.``テスト名`` パラメータリスト =
        // テストコード
```

以下のように利用します。

```fsharp
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`** を利用します。

```fsharp
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の使い方になります。
まだ他にもいろいろな機能がありますので、随時追記していきたいと思います。