Enhetstester i Clojure

Da jeg presenterte kassaapparat-kataen i clojure sa jeg at jeg ikke la vekt på enhetstester. Det betyr derimot ikke at jeg ikke skrev tester i det hele tatt.

Clojure kommer med et eget namespace for å skrive enhetstester, og fornuftig nok heter det clojure.test. Tidligere var dette kun en del av clojure-contrib, men det har nå blitt tatt opp i selve Clojure. For å ta det i bruk benytter jeg først ns-funksjonen til å definere et namespace i filen min, og kan der inkludere avhengigheter jeg ønsker å benytte, slik som dette:

  1 (ns marosoft.kata.cash-register
  2     (:use clojure.test))

For å skrive enhetstestene er det så kun to ting du trenger å vite om: Du definerer en test ved hjelp av deftest. En test ser ut som en vanlig funksjon, men uten parametre. Når du skal gjøre en “assert” i testen bruker du makroen is. Hvis argumentet til is ikke evaluerer til true vil testen feile.

Her er testene jeg skrev for basisfunkjonaliteten i kassaapparat-kataen:

106 ; — TESTS —
107
108 (deftest test-that-price-returns-correct-values
109          (is (= 3.99 (price “bread”)))
110          (is (= 2.50 (price “milk”)))
111          (is (= 4.00 (price “butter”))))
112
113 (deftest price-for-2-milk
114          (is (= (* 2.50 2)
115                 (price-for-selection “milk” 2))))
116
117 (deftest price-for-3-bread-using-overload
118          (is (= (* 3.99 3)
119                 (price-for-selection ["bread" 3]))))
120
121 (deftest total-of-a-bread-2-milk-3-butter
122          (is (= (+ 3.99 (* 2 2.50) (* 3 4.00))
123                 (total [["bread" 1]["milk" 2]["butter" 3]]))))
124
125 (deftest buy-will-update-cart
126          (let [original-cart
127                 [["bread" 2]]
128                 updated-cart
129                 (buy “butter” 1 original-cart)]
130            (is (= 1 (count original-cart)))
131            (is (= 2 (count updated-cart)))
132            (is (= 1 (count (filter
133                              #(= (first %) “butter”) 
134                              updated-cart))))))
135
136 (deftest cart-will-not-be-cleared-if-not-enough-money
137          (let [updated-cart (checkout 10.0 [["milk" 10]])]
138            (is (= 1 (count updated-cart)))))
139
140 (deftest cart-will-be-cleared-if-enough-money
141          (let [updated-cart (checkout 100.0 [["milk" 10]])]
142            (is (= 0 (count updated-cart)))))
143

For å kjøre testene kaller du den innebygde funkjonen run-tests ved for eksempel å skrive (run-tests) i bunnen av fila. Da jeg implementerte kataen min gjorde jeg det derimot litt anderledes…

Integrere testene som en del av programmet

Da jeg implementerte kassaapparatet skrev jeg testene i samme fil som selve programmet. Jeg utvidet så brukerens meny til å inkludere en opsjon for å kjøre alle testene. Dette gjorde jeg ved å legge linjen nedenfor til i dispatch-command listen (se forrige blogpost).

72      “test” (fn [state] (do (run-tests) (System/exit 0)))

Jeg kunne dermed starte programmet og skrive ordet “test” for å kjøre testene. Jeg valgte også å la programmet avslutte etter at testene var kjørt ved å eksekvere (System/exit 0), som er Java interop og tilsvarer å kalle System.exit(0).

Kult?

Flere muligheter

is-makroen kan ta en ekstra streng-parameter som beskriver hva som testes, om du liker å gjøre det. I tillegg finnes det en testing-makro som lar deg definere mere beskrivende tester, ala BDD / RSpec.

Man kan også definere test fixtures, som i praksis gir deg setup (before) og teardown (after) logikk. Og man kan komponere ulike sett av tester, og spesifisere hvilke namespace man ønsker å kjøre tester for når man kaller run-tests.

Og man kan definere en funksjon og testene for funksjonen i par, slik at det ikke er noen avstand mellom testen og koden. Bruker man dette vil man nok tvinge seg selv til å gjennomføre ganske god TDD. Man kan også sette et flagg som dropper testene under kompilering, slik at det å ha testene sammen med produksjonkoden ikke er noen issue.

Her har jeg laget en funksjon som er definert sammen med testene sine, og hvor jeg også bruker testing-makroen for å gruppere (det gir også bedre rapportering ved feil):

146 (with-test
147   (defn can-watch-movie?
148         “Can person watch movie based on MPAA rating?”
149         ([rating age] ; overload 1 (defaults to no adult supervision)
150          (can-watch-movie? rating age false))
151         ([rating age with-adult?] ; overload 2..
152          (condp = rating
153                 “R” (or (>= age 17) with-adult?)
154                 “NC-17″ (> age 17)
155                 true))) ; defaults to true for all other ratings
156   (testing “Non-restrictive ratings”
157            (is (true? (can-watch-movie? “G” 1 false)))
158            (is (true? (can-watch-movie? “PG” 2 false)))
159            (is (true? (can-watch-movie? “PG-13″ 12 false))))
160   (testing “Restricted rating (adult guardian required if under 17)”
161            (is (false? (can-watch-movie? “R” 16 false)))
162            (is (true? (can-watch-movie? “R” 16 true)))         
163            (is (true? (can-watch-movie? “R” 17 false))))
164   (testing “NC-17 – No One 17 and Under Admitted”
165            (is (false? (can-watch-movie? “NC-17″ 17 false)))
166            (is (false? (can-watch-movie? “NC-17″ 17 true)))
167            (is (true? (can-watch-movie? “NC-17″ 18 false))))
168   (testing “Using overload without specifying supervision”
169            (is (false? (can-watch-movie? “R” 16)))))

For en fullstendig oversikt over hva du har tilgjengelig kan du ta en titt på clojure.test API-referansen.

Kategorier: LISP/Clojure, Testing / TDD.
RSS feed for kommentarene. Tilbaketråkk.

Skriv en kommentar

Tillatte tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


Einar W. Høst: Det er jo læringen som gjør det morsomt! Se også http://norvig.com/21-days...

Pagliacci: OBS! tl;wr. Det er vel akuratt det jeg sliter med med min læring innenfor pr...

Torbjørn: La oss anta to ulike definisjoner av Template Method pattern - de to ytterpunkte...

Lars-Petter: Hei igjen. Siden du inviterer til å ta diskusjonen i bloggen, og har tatt deg t...

Torbjørn: Lars-Petter: Det er én måte å se det på. Det er absolutt fortsatt Template M...

Lars-Petter: Hei. Har du ikke i prinsippet her gått over fra Template Method (arv) til Strat...

Christian Abildsø: I alle fall i C#, så føles dette som kode som blir mer fleksibel men vanskelig...

Torbjørn: Hei Henrik, og takk for tilbudet. Ble oppmerksom på Rasberry Pi for under en uk...

Henrik Sandaker Palm: Ang. større hobby prosjekt. Du er som er en slik rakker på programmering har j...

Øivind Nilsen: Slutt å bruke mobilnummeret mitt som eksempel !...

Mulig relaterte linker

 Hold deg oppdatert

Søk i bloggen

Ferske innlegg

  • En historie om programmering
  • Template Method del 4: Multippel arv
  • Template Method Intermesso
  • Template Method del 3: Bare funksjoner
  • Kategorier

  • .net ninja (37)
  • Bøker (17)
  • Diverse prosjekter (35)
  • DSL (10)
  • Erlang (10)
  • F# (5)
  • Hardware (1)
  • Jobb (77)
  • Julekalender (51)
  • kjempekjekt.com (23)
  • LISP/Clojure (33)
  • NNUG / community (60)
  • O/RM & databaser (10)
  • Off topic (116)
  • OO-design/clean code (30)
  • Podcasts (14)
  • Polyglot (77)
  • Ruby (27)
  • Silverlight / RIA (3)
  • Software/verktøy (20)
  • Softwareutvikling (21)
  • Testing / TDD (30)
  • the contiki strip (13)
  • User experience (3)
  • WCF (3)
  • Webutvikling (32)
  • WPF (9)
  • WTF (12)
  • Last ned Wallpaper

    Programmeringsbloggens tøffe skrivebordsbakgrunn med snippets fra ulike språk laster du ned her!

    Abonner via epost

    Om du vil kan du få alle nye blogposter tilsendt til din epost. Abonner nå, det er kjempeenkelt!

    Meta