2016-07-19 109 views
11

In che modo i programmatori funzionali testano le funzioni che restituiscono un'unità?In che modo i programmatori funzionali testano le funzioni che restituiscono un'unità?

Nel mio caso, credo che ho bisogno di unit test un'interfaccia per questa funzione:

let logToFile (filePath:string) (formatf : 'data -> string) data = 
    use file = new System.IO.StreamWriter(filePath) 
    file.WriteLine(formatf data) 
    data 

Qual è l'approccio consigliato quando sono unità di test di una funzione con I/O?

In OOP, credo che un Test Spy possa essere sfruttato.

Il pattern Spia di prova si traduce in programmazione funzionale?

Il mio cliente simile a questa:

[<Test>] 
let ``log purchase``() = 
    [OneDollarBill] |> select Pepsi 
        |> logToFile "myFile.txt" (sprintf "%A") 
        |> should equal ??? // IDK 

Il mio dominio è la seguente:

module Machine 

type Deposit = 
    | Nickel 
    | Dime 
    | Quarter 
    | OneDollarBill 
    | FiveDollarBill 

type Selection = 
    | Pepsi 
    | Coke 
    | Sprite 
    | MountainDew 

type Attempt = { 
    Price:decimal 
    Need:decimal 
} 

type Transaction = { 
    Purchased:Selection 
    Price:decimal 
    Deposited:Deposit list 
} 

type RequestResult = 
    | Granted of Transaction 
    | Denied of Attempt 

(* Functions *) 
open System 

let insert coin balance = coin::balance 
let refund coins = coins 

let priceOf = function 
    | Pepsi 
    | Coke 
    | Sprite 
    | MountainDew -> 1.00m 

let valueOf = function 
    | Nickel   -> 0.05m 
    | Dime   -> 0.10m 
    | Quarter  -> 0.25m 
    | OneDollarBill -> 1.00m 
    | FiveDollarBill -> 5.00m 

let totalValue coins = 
    (0.00m, coins) ||> List.fold (fun acc coin -> acc + valueOf coin) 

let logToFile (filePath:string) (formatf : 'data -> string) data = 
    let message = formatf data 
    use file = new System.IO.StreamWriter(filePath) 
    file.WriteLine(message) 
    data 

let select item deposited = 
    if totalValue deposited >= priceOf item 

    then Granted { Purchased=item 
        Deposited=deposited 
        Price = priceOf item } 

    else Denied { Price=priceOf item; 
        Need=priceOf item - totalValue deposited } 
+3

Le funzioni di stile di programmazione funzionale non hanno effetti collaterali. Poiché l'unica ragione plausibile per chiamare una funzione di ritorno all'unità diversa da "ignora" è per gli effetti collaterali, non ha senso chiedere quale sia il modo funzionale per testare un tipo di funzione che non si adatta allo stile funzionale . –

+1

@JoelMueller Se ci pensate, lo stesso vale per Haskell dove non ci sono effetti collaterali, gli effetti sono espliciti nel sistema dei tipi. Non esiste ancora un modo semplice per testarli e il consiglio è ancora di limitare l'IO a un piccolo sottoinsieme del codice. Forse uno degli aspetti più utili della monade IO è semplicemente che se tutte le tue funzioni finiscono in 'IO', mostra che probabilmente stai facendo qualcosa di sbagliato! – TheInnerLight

risposta

12

Non vedo questa risposta come una risposta autorevole, perché non sono un esperto in testing, ma la mia risposta a questa domanda sarebbe che, in un mondo perfetto, non è possibile e non è necessario testare le funzioni unit -returning .

Idealmente, si potrebbe strutturare il codice in modo che esso è composto da qualche IO leggere i dati, le trasformazioni che codificano per tutta la logica e alcuni IO per salvare i dati:

read 
|> someLogic 
|> someMoreLogic 
|> write 

L'idea è che tutti i dati importanti le cose sono in someLogic e someMoreLogic e che read e write sono completamente banali - leggono il file come stringa o sequenza di linee. Questo è abbastanza banale che non è necessario testarlo (ora è possibile testare la scrittura effettiva del file leggendo nuovamente il file, ma è quando si desidera testare il file IO piuttosto che qualsiasi logica scritta) .

Questo è dove si usa un modello in OO, ma dal momento che si dispone di una bella struttura funzionale, si sarebbe ora scrivere:

testData 
|> someLogic 
|> someMoreLogic 
|> shouldEqual expectedResult 

Ora, in realtà, il mondo non è sempre così bello e qualcosa come un'operazione spy si rivela utile - forse perché interagisci con un mondo che non è puramente funzionale.

Phil Trelford ha uno nice and very simple Recorder che consente di registrare le chiamate a una funzione e controllare che sia stato chiamato con gli input previsti - e questo è qualcosa che ho trovato utile un certo numero di volte (ed è abbastanza semplice che tu non ho davvero bisogno di un quadro).

+1

Questo è abbastanza vicino alla risposta che penso di scrivere me stesso. Nei test unitari, di solito operiamo con [Humble Objects] (http://xunitpatterns.com/Humble%20Object.html), che sono codice di implementazione che è stato così prosciugato dalla logica che non è necessario testarli - e è lì che metti il ​​tuo I/O. Farei lo stesso in F # e Haskell. –

+0

Se non si riesce a svuotare una funzione di logica efficace, un'alternativa funzionale all'unità di ritorno sarebbe utilizzare le monade * State * o * Writer *. –

9

Ovviamente, si potrebbe usare un finto come si farebbe in codice imperativo fintanto l'unità di codice prende le sue dipendenze come parametro.

Ma, per un altro approccio, ho trovato questo discorso davvero interessante Mocks & stubs by Ken Scambler. Per quanto ricordo, l'argomentazione generale era che dovresti evitare di usare i mock mantenendo tutte le funzioni il più puri possibile, rendendoli dati in-data-out. Ai margini del tuo programma, avresti alcune funzioni molto semplici che eseguono gli importanti effetti collaterali. Questi sono così semplici che non hanno nemmeno bisogno di test.

La funzione che hai fornito è abbastanza semplice da rientrare in quella categoria. Provarlo con un finto o simile implicherebbe solo assicurando che determinati metodi vengano chiamati, non che l'effetto collaterale si sia verificato. Tale test non è significativo e non aggiunge alcun valore al codice stesso, aggiungendo comunque un onere di manutenzione. È meglio testare la parte con effetti collaterali con un test di integrazione o test end-to-end che guardi effettivamente il file che è stato scritto.

Un'altra buona discussione sull'argomento è Boundaries by Gary Bernhardt che discute il concetto di Functional Core, Imperative Shell.

Problemi correlati