a wandering wolf

Does a wandering wolf dreams of a wondering, sometimes programming sheep?

このエントリーをはてなブックマークに追加

Persimmon のテストで成功と失敗の両方を扱う

前書き

Persimmon では普通のテストだけでなく、いわゆるパラメタライズド・テストを書くことも出来ます。パラメタライズド・テストは皆さんご存じの通り、パラメータをテストの外から渡すことにより、1つのテストで複数のテストケースに対応することが出来ます。

let ``your nice test`` =
    let parameterizedTest(input, expected) = test {
        let actual = fut input   (* fut はテスト対象の関数 *)
        assertEquals expected actual
    }
    parameterize {
        case ("F#!F#!", true)   (* ここのタプルが、下でrunしているテスト関数の引数になる *)
        case ("Fortlan", false)
        run parameterizedTest
    }

たいていのケースでは問題にならないんですが、あるパラメータでは例外が発生してほしい、というようなニーズ(そういう設計が良いかどうか、は今回議論しません)がある場合もあります。Persimmon では trap コンピュテーション式でテスト対象が投げた例外を取得することが出来ます。しかし、別のパラメータでは例外が発生しないようなテストケースを用意していた場合、テスト対象が例外を投げないとテスト失敗になってしまいます。

対応策

今回のケースでは、Either または Success/Failure のような型を用意することで、テスト可能になります。F# では、Choice<’a, ‘b> というのが Either に近い型構造をしているので、これを使ってみます:

let catch (expr: Lazy<'T>) =
    try
        Choice2Of2 (expr.Force())
    with
        | ex -> Choice1Of2(ex)
let assertEither (expected: Choice<#exn, _>) (actual: Choice<#exn, _>) =
    match expected, actual with
    | (Choice1Of2 ex, Choice1Of2 ex') ->
        let resultOfTypeChecking = assertEquals (ex.GetType()) (ex'.GetType())
        match resultOfTypeChecking with
        | AssertionResult.Passed _ -> assertEquals ex.Message ex'.Message
        | _                        -> resultOfTypeChecking
    | (Choice2Of2 value, Choice2Of2 value') ->
        assertEquals value value'
    | let message = sprintf "Expect: %A\nActual: %A" expected actual
      AssertionResult.NotPassed(NotPassedCause.Violated message)

このようなユーティリティ関数を用意しておくと、次のようにテストを書くことが出来ます:

let ``my humble test`` =
    let parameterizedTest(input, expected) = test {
        let actual = catch (Lazy.Create <| fun () -> fut input)
        do! assertEither expected actual
    }
    parameterize {
        case ("F#!F#!", Choice2Of2 true)
        case ("Fortlan", Choice2Of2 false)
        case ("fhsarp", Choice1Of2 <| System.ArgumentException("wrong language name or you typoed?"))
        run parameterizedTest
    }

catch 関数はテスト対象を実行し、例外が発生した場合は発生した例外を Choice1Of2 に包み、正常終了した場合は結果を Choice2Of2 に包んで返します。catch 関数の引数に Lazy<’T> 型を取っているのは、catch 関数にテスト対象を渡した時にその場で例外が発生しないようにするためです。

assertEither 関数では、期待値と結果がどちらも例外か、またはどちらも正常値かを確認し、判定します。例外の場合は例外型とメッセージだけを確認しています。渡された引数が期待値と正常値で不一致であった場合、もちろんテスト失敗にしています。

後書き

1つのテストで正常値と例外の確認をする方法を見てみました。このやり方は @bleis さんからアドバイスをいただいた結果になります:

いつもお世話になってます!!!

あと最近の傾向として、柿エバンジェリストみたいな記事ばっか書いてる感あります。あくまで私の備忘録的なものなんですが、テストが書きやすいんだからSHOUGANAI。