2013-09-25 12 views
6

Chciałbym sprawdzić, czy wartość dotyczy konkretnego przypadku dyskryminowanej unii, bez konieczności sprawdzania żadnych uwzględnionych danych. Moją motywacją jest testowanie tylko jednej rzeczy przy każdym teście jednostkowym.Jak sprawdzić przypadek dyskryminowanej unii z FsUnit?

Przykładem jest następujący (dwie ostatnie linie nadają błędy kompilacji):

module MyState 

open NUnit.Framework 
open FsUnit 

type MyState = 
    | StateOne of int 
    | StateTwo of int 

let increment state = 
    match state with 
    | StateOne n when n = 10 -> StateTwo 0 
    | StateOne n -> StateOne (n + 1) 
    | StateTwo n -> StateTwo (n + 1) 

[<Test>] 
let ``incrementing StateOne 10 produces a StateTwo``()= 
    let state = StateOne 10 
    (increment state) |> should equal (StateTwo 0)    // works fine 
    (increment state) |> should equal (StateTwo _)    // I would like to write this... 
    (increment state) |> should be instanceOfType<StateTwo> // ...or this 

można to zrobić w FsUnit?

Jestem świadomy this answer, ale wolałbym nie pisać odpowiednich funkcji dla każdego przypadku (w moim prawdziwym kodzie jest więcej niż dwa).

+0

Tu jest rzeczywiście dość łatwy sposób to zrobić z C#, ale to nie działa w F #. –

Odpowiedz

7

Jeśli nie przeszkadza przy użyciu odbicia, funkcja isUnionCase z this answer może być przydatny:

increment state 
|> isUnionCase <@ StateTwo @> 
|> should equal true 

Należy pamiętać, że jest to nieco rozwlekły, ponieważ trzeba wywołanie funkcji przed porównaniem wartości.

Podobny ale lżejsze podejście mogłoby być porównanie z tagami:

// Copy from https://stackoverflow.com/a/3365084 
let getTag (a:'a) = 
    let (uc,_) = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(a, typeof<'a>) 
    uc.Name 

increment state 
|> getTag 
|> should equal "StateTwo" 

Pamiętaj, że nie jest to typ bezpieczny i można łatwo błędnie nazwę sprawy unii.

Co chciałbym zrobić jest stworzenie podobnego DUS dla celów porównawczych:

type MyStateCase = 
    | StateOneCase 
    | StateTwoCase 

let categorize = function 
    | StateOne _ -> StateOneCase 
    | StateTwo _ -> StateTwoCase 

W ten sposób można określić categorize raz i używać go wielokrotnie.

increment state 
|> categorize 
|> should equal StateTwoCase 
0

To nie wygląda bardzo elegancki, ale można wyodrębnić typ od wartości stanu:

let instanceOfState (state: 'a) = 
    instanceOfType<'a> 

a następnie używać go w teście:

(increment state) |> should be (instanceOfState <| StateTwo 88) 

EDIT

Tak, niestety typem jest zawsze MyS tate. Wygląda na to, że dopasowanie wzoru lub brzydka refleksja są nieuniknione.

+0

To nie działa - test przechodzi, nawet jeśli używam 'StateOne 88', więc nie sprawdza, czy jest to wartość' StateTwo'. –

+1

To nie działa, ponieważ typ '' a' będzie po prostu 'MyState'. Będzie działać, jeśli 'instanceOfState' użyje informacji o typie środowiska wykonawczego na temat parametru' state', ale to oznacza, że ​​musi działać nieco inaczej ... –

1

Wygląda na to, że FSUnit nie (lub nie mogę, nie jestem pewien) bezpośrednio obsługuje ten przypadek użycia.

Następną najlepszą rzeczą jaką znalazłem jest zadeklarowanie typu TestResult jak poniżej i użycie dopasowania w celu zmniejszenia wyniku do tego typu.

type TestResult = 
| Pass 
| Fail of obj 

Oto zmniejszenie mecz

let testResult = 
    match result with 
    | OptionA(_) -> Pass 
    | other -> Fail(other) 

Teraz możesz po prostu użyć should equal celu zapewnienia prawidłowego rezultatu.

testResult |> should equal Pass 

Korzyści z tego rozwiązania są silne typowanie ale co ważniejsze w przypadku awarii można zobaczyć co nieprawidłowy wynik był.

0

Co się stanie, jeśli FsUnit obsługuje już twierdzenie dotyczące określonego przypadku powiązania, choć ogranicza się do wartości typu Microsoft.FSharp.Core.Choice<_,...,_>?

Wykorzystajmy to za pomocą aktywnego wzorca wielozadaniowego, który wykorzystuje odbicie, aby sprawdzić nazwę przypadku związku.

open System.Reflection 
open Microsoft.FSharp.Reflection 

let (|Pass|Fail|) name (x : obj) = 
    let t = x.GetType() 
    if FSharpType.IsUnion t && 
     t.InvokeMember("Is" + name, 
      BindingFlags.GetProperty, null, x, null) 
     |> unbox then Pass 
    else Fail x 

powinno działać teraz:

increment state 
|> (|Pass|Fail|) "StateTwo" 
|> should be (choice 1)