2013-08-20 13 views
9

Sto provando a scrivere alcuni test unitari per la funzione clojure (sto usando clojure.test, ma posso passare a midje se necessario).Test dell'unità Clojure: Verifica se una funzione è stata chiamata

Ho una funzione che si legge come:

(defn GenerateNodes 
    [is-sky-blue? hot-outside? name] 
    (cond 
    (is-sky-blue? name) (generate-sky-nodes) 
    (hot-outside?) (generate-hot-nodes))) 

quando l'unità di testare questa funzione, voglio scrivere il seguente banco di prova:

(deftest when-sky-blue-then-generate-sky-nodes 
    (let [is-sky-blue true] 
     (GenerateNodes (fn[x] println "sky nodes generated.")) 
      (is (= true Was-generate-hot-nodes-called?)) 

Come posso affermare che la funzione generare- sono stati chiamati i nodi del cielo? o no ? Vorrei usare un framework di simulazione in C# o java, ma non conosco il clojure.

+4

Buona domanda. Mi sembra che tu stia provando ad applicare un imperativo test di stile al codice dichiarativo.Non dovresti descrivere * come * le cose funzionano, ma * cosa * fanno, quindi una funzione che viene chiamata è IMO un dettaglio irrilevante. Per essere confermato dagli esperti di programmazione funzionale però (che io non sono). – guillaume31

+1

@ guillaume31, penso che questa sia la differenza tra mock e stub. Gli stub servono solo a fornire false implementazioni del comportamento di supporto, mentre i mock lo fanno e fanno anche contabilità. Personalmente trovo che i mock siano eccezionalmente cattivi, anche nel mondo OO. Doubly so in functional world. Ma potrebbe essere solo io. – ivant

+0

@ivant Dunno. Credo che gli stub descrivono ancora in qualche modo * come *, anche se probabilmente non puoi farne a meno per ottenere test performanti. Mazze Personalmente trovo utile, non per fare micro-contabilità, ma per verificare che un oggetto non parli maleducato (cioè il protocollo esterno) a uno dei suoi colleghi, rendendo la mancanza di un'applicazione fluente di questi protocolli nei sistemi di tipi OO. – guillaume31

risposta

8

Quello che hai già non è lontano da una versione funzionante. Ho cambiato le cose un po 'per essere più idiomatico di Clojure.

Di seguito si presuppone che generano-sky-nodi e generare-hot-nodi ogni ritorno certo valore (questo può essere fatto in aggiunta a eventuali effetti collaterali che hanno), vale a dire:

(defn generate-sky-nodes 
    [] 
    (doseq [i (range 10)] (do-make-sky-node i)) 
    :sky-nodes) 

allora, la tua generare nodi viene regolata come segue:

(defn generate-nodes 
    [sky-blue? hot-outside? name] 
    (cond 
    (sky-blue? name) (generate-sky-nodes) 
    (hot-outside?) (generate-hot-nodes))) 

e, infine, la versione funzionale delle prove:

(deftest when-sky-blue-then-generate-sky-nodes 
    (let [truthy (constantly true) 
     falsey (constantly false) 
     name nil] 
    (is (= (generate-nodes truthy falsey name) 
     :sky-nodes)) 
    (is (= (generate-nodes truthy truthy name) 
     :sky-nodes)) 
    (is (not (= (generate-nodes falsey falsey name) 
       :sky-nodes))) 
    (is (not (= (generate-nodes falsey truthy name) 
       :sky-nodes))))) 

L'idea generale è che non si prova ciò che ha fatto, si prova ciò che restituisce. Quindi disponi il tuo codice in modo tale che (quando possibile) tutto ciò che conta su una chiamata di funzione è ciò che restituisce.

Un ulteriore suggerimento è quello di minimizzare il numero di luoghi in cui avvengono gli effetti collaterali utilizzando generate-sky-nodes e generate-hot-nodes di restituire l'effetto collaterale da effettuare:

(defn generate-sky-nodes 
    [] 
    (fn [] 
    (doseq [i (range 10)] (do-make-sky-node i)) 
    :sky-nodes)) 

e la chiamata di generate-nodes sarà simile alla seguente :

(apply (generate-nodes blue-test hot-test name) []) 

o più brevemente (anche se certamente strano se si hanno meno familiarità con Clojure):

((generate-nodes blue-test hot-test name)) 

(mutatis mutandis, nel codice di prova sopra i test funzionano con questa versione così)

+0

Questo è un ottimo esempio per qualcuno che non ha ancora familiarità con il paradigma funzionale. +1 –

+0

Ottimo post! Esiste un nome per questo modello per cui le funzioni di effetto collaterale restituiscono un "token" che aiuta a testare? – camdez

9

È possibile scrivere una macro per simulare funzioni e verificare se una funzione è stata chiamata o meno. Oppure puoi usare la libreria expect-call.

(defn check-error [a b] 
    (when (= a :bad-val) 
    (log :error b))) 

; This will pass 
(deftest check-logging 
    (with-expect-call (log [:error _]) 
    (check-error :bad-val "abc"))) 

; This will fail 
(deftest check-logging-2 
    (expect-call (log [:error _]) 
    (check-error :good-val "abc"))) 
3

Utilizzando Midje s' checkables:

(unfinished is-sky-blue? hot-outside?) 
(facts "about GenerateNodes" 
    (fact "when the sky is blue then sky nodes are generated" 
    (GenerateNodes is-sky-blue? hot-outside? ..name..) => ..sky-nodes.. 
    (provided 
     (is-sky-blue? ..name..) => true 
     (generate-sky-nodes) => ..sky-nodes.. 
     (generate-hot-nodes) => irrelevant :times 0))) 
0

È possibile utilizzare mock-clj.

(require ['mock-clj.core :as 'm]) 

(deftest when-sky-blue-then-generate-sky-nodes 
    (m/with-mock [is-sky-blue? true 
       generate-sky-nodes nil] 
    (GenerateNodes (fn[x] println "sky nodes generated.") ... ...) 
    (is (m/called? generate-sky-nodes))))