2013-08-20 10 views
9

Ich versuche, einige Komponententests für meine Clojure-Funktion zu schreiben (ich benutze clojure.test, aber ich kann bei Bedarf zu midje wechseln).Clojure Unit Test: Überprüfen Sie, ob eine Funktion aufgerufen wurde

Ich habe eine Funktion, die wie lautet:

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

, wenn das Gerät diese Funktion zu testen, habe ich den folgenden Testfall schreiben möchten:

(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?)) 

Wie kann ich, dass die Funktion generate behaupten Sky-Nodes wurde genannt? oder nicht ? Ich würde ein spöttisches Framework in C# oder Java verwenden, aber ich weiß nichts über Clojure.

+4

Gute Frage. Es scheint mir, dass Sie versuchen, imperativen Stiltest auf deklarativen Code anzuwenden.Du sollst nicht beschreiben * wie * Dinge funktionieren, aber * was * sie tun, also ist eine aufgerufene Funktion IMO ein irrelevantes Detail. Allerdings von funktionalen Programmierern bestätigt zu werden (was ich nicht bin). – guillaume31

+1

@ guillaume31, ich denke das ist der Unterschied zwischen Mocks und Stubs. Stubs sind nur dazu da, gefälschte Implementierungen von unterstützendem Verhalten zu liefern, während Mocks das tun und auch Buchhaltung leisten. Persönlich finde ich Mocks als außergewöhnlich schlechte Idee, sogar in der OO-Welt. Doppelt so in der funktionalen Welt. Aber es kann nur ich sein. – ivant

+0

@ivant Dunno. Ich denke, Stubs beschreiben irgendwie immer noch das * wie *, obwohl Sie wahrscheinlich nicht ohne sie auskommen können, um performante Tests zu erhalten. Sprüche, die ich persönlich finde, sind nützlich, um Mikro-Accounting nicht durchzuführen, sondern um zu verifizieren, dass ein Objekt nicht unhöflich (d. H. Externes Protokoll) zu einem seiner Peers spricht, was das Fehlen einer reibungslosen Durchsetzung dieser Protokolle in OO-Typsystemen bewirkt. – guillaume31

Antwort

8

Was Sie haben, ist schon nicht weit von einer Arbeitsfunktionsfähige Version. Ich habe die Dinge etwas verändert, um Clojure idiomatischer zu sein.

Im Folgenden wird angenommen, dass die Erzeugung-Himmel-Knoten und erzeugen Hot-Knoten jeweils einen Wert zurückgeben (dies kann zusätzlich zu irgendwelchen Nebenwirkungen durchgeführt werden sie haben), dh:

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

dann, Ihre generieren-Knoten wird wie folgt angepasst:

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

und schließlich die funktionsfähige Version der Tests:

(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))))) 

Die allgemeine Idee ist, dass Sie nicht testen, was es getan hat, Sie testen, was es zurückgibt. Dann ordnen Sie Ihren Code so an, dass (wann immer möglich) alles, was für einen Funktionsaufruf wichtig ist, zurückgegeben wird.

Ein weiterer Vorschlag ist, die Zahl der Plätze zu minimieren, wo Nebenwirkungen passieren durch generate-sky-nodes und generate-hot-nodes mit der Nebenwirkung zurückzukehren durchgeführt werden:

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

und Ihren Anruf von generate-nodes würde wie folgt aussehen :

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

oder kurz und bündig (obwohl zugegebenermaßen merkwürdig, wenn Sie weniger vertraut mit Clojure sind):

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

(mutatis mutandis im obigen Test Code die Tests auch mit dieser Version arbeiten)

+0

Dies ist ein großartiges Beispiel für jemanden, der mit dem funktionalen Paradigma noch nicht vertraut ist. +1 –

+0

Toller Beitrag! Gibt es einen Namen für dieses Muster, bei dem Nebeneffekte einen "Token" zurückgeben, der beim Testen hilft? – camdez

9

Sie können ein Makro selbst schreiben, um Funktionen zu simulieren und prüfen, ob eine Funktion aufgerufen wurde oder nicht. Oder Sie können expect-call Bibliothek verwenden.

(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

Mit 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

Sie mock-clj verwenden können.

(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))))