8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

F#Advent Calendar 2019

Day 21

F#でxUnitを使う方法

Last updated at Posted at 2019-12-21

はじめに

アドカレの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.slnSampleConsoleプロジェクト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 でビルド・実行ができる環境を整えます。
詳細な方法は こちら を参照してください。

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

image.png

image.png

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

image.png

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

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

image.png

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

image.png

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

image.png

テストを実施してみる

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

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関数 は、引数で渡された 期待値実績値 が一致している場合にテストをパスすることができます。
このようなテスト用の関数は他にも複数用意されています。それについては後述します。

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

スクリーンショット 2019-12-21 17.20.25.png

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

スクリーンショット 2019-12-21 17.24.48.png

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

スクリーンショット 2019-12-21 17.26.29.png

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

スクリーンショット 2019-12-21 17.28.34.png

このように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かどうかチェック の検査をします。
conditiontrue の場合にテストをパスすることができます。

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

False (condition)

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

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

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

8
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?