2013-09-25 14 views
6

Vorrei verificare che un valore sia un caso particolare di unione discriminata, senza dover controllare anche i dati inclusi. La mia motivazione è di testare solo una cosa con ogni test unitario.Come verificare il caso di un'unione discriminata con FsUnit?

Un esempio è la seguente (le ultime due righe danno errori di compilazione):

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 

si può fare in FsUnit?

Sono a conoscenza di this answer ma preferirei non dover scrivere le funzioni di corrispondenza per ciascun caso (nel mio codice reale ce ne sono più di due).

+0

V'è in realtà un modo abbastanza facile da fare questo da C#, ma non funziona in F #. –

risposta

7

Se non ti dispiace utilizzando riflessioni, la funzione isUnionCase da this answer potrebbe essere a portata di mano:

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

Nota che è un po 'prolisso perché avete bisogno di una chiamata di funzione prima di confrontare i valori.

Un approccio simile, ma più leggero potrebbe essere il confronto di tag:

// 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" 

Attenzione che questo non è di tipo-safe e si può facilmente errori ortografici un nome caso unione.

Quello che vorrei fare è di creare un DUs simile a scopo di confronto:

type MyStateCase = 
    | StateOneCase 
    | StateTwoCase 

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

In questo modo, si definisce categorize volta e utilizzarlo più volte.

increment state 
|> categorize 
|> should equal StateTwoCase 
0

Non sembra molto elegante, ma è possibile estrarre il tipo da un valore di stato:

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

e poi utilizzarlo nel test:

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

EDIT

Sì, purtroppo il tipo è sempre MyS tate. Sembra che il pattern matching o il brutto riflesso siano inevitabili.

+0

Questo non sembra funzionare - il test passa anche se utilizzo 'StateOne 88', quindi non controllo che il valore sia uno' StateTwo'. –

+1

Questo non funziona perché il tipo ''a' sarà semplicemente' MyState'. Funzionerebbe se 'instanceOfState' usasse informazioni sul tipo di runtime sul parametro' state', ma questo significa che ha bisogno di lavorare un po 'diversamente ... –

1

Sembra che FSUnit non (o non possa, non sono sicuro) supporta direttamente questo caso d'uso.

La prossima cosa migliore che ho trovato è dichiarare un tipo TestResult come il seguente e utilizzare una corrispondenza per ridurre il risultato a questo tipo.

type TestResult = 
| Pass 
| Fail of obj 

Ecco la riduzione partita

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

Ora si può semplicemente utilizzare should equal per garantire il risultato corretto.

testResult |> should equal Pass 

I vantaggi di questa soluzione sono tipizzazione forte ma ancora più importante nel caso fallimento si può vedere ciò che il risultato non valido è stato.

0

Cosa succede se FsUnit supporta già un'asserzione rispetto a uno specifico caso unione, benché limitata a valori del tipo Microsoft.FSharp.Core.Choice<_,...,_>?

Sfruttiamo questo con un modello attivo a più casi, che utilizza Reflection per verificare il nome del caso di unione.

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 

dovrebbe funzionare ora:

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