Zobrazují se příspěvky se štítkemunit testy. Zobrazit všechny příspěvky
Zobrazují se příspěvky se štítkemunit testy. Zobrazit všechny příspěvky

2. dubna 2012

Lepší testování v Clojure. Midje

Dneska navážu na unit testování v Clojure, nicméně dostanu se k tomu oklikou. Minulé léto se celkem hojně psalo o desátém výročí deklarace Manifesto for Agile Software Development. Internetem (a mojí čtečkou) tehdy protekly nějaké rozhovory s někdejšími účastníky/signatáři. Jedním z nich byl i Brian Marick, autor Midje, testovacího frameworku pro Clojure (ano, to je to Midje, které jsem zmiňoval ve svém postu o ThoughtWorks Radar).

Instalace/zprovoznění

Midje je nejjednodušší začít používat pomocí Leiningenu (určitě jeden z příštích zápisků). Definice projektu může vypadat třeba takto:
(defproject blog-midje "1.0.0-SNAPSHOT"
            :description "Midje for Sometimes Clojure"
            :dependencies [[org.clojure/clojure "1.3.0"]]
            :dev-dependencies [[midje "1.3.2-SNAPSHOT"]])
REPL se pak spustí příkazem lein repl.

Fakta

Základem testování v Midje jsou fakta. Fakta o budoucím kódu (test first):
(ns blog-midje (:use midje.sweet))
; nil
(fact (+ 1 1) => 2)
; true
(fact 1 => odd?)
; true
(fact "truth about one" 1 => even?)
; FAIL at (NO_SOURCE_FILE:11)
; Actual result did not agree with the checking function.
;         Actual result: 1
;     Checking function: even?
; false
Fakt může mít více klauzulí. Dá se použít makro fact nebo facts:
(facts "about42"
       (+ 21 21) => 42
       (* 6 7) => 42)
; true
Pokud je potřeba fakt prezentovat pomocí sady hodnot, nabízí Midje tzv. tabulková fakta:
(defn xor [x y]
  (if (= x y) false true))
; #'blog-midje/xor

(tabular
  (fact "The rules of XOR"
        (xor ?x ?y) => ?expected)
  ?x    ?y    ?expected
  0     0     false
  0     1     true
  1     0     true
  1     1     false
  false false false
  false true  true
  true  false true
  true  true  false)
; true
Vyhození výjimky se dá otestovat pomocí funkce (checkeru) throws:
(fact (/ 42 0) => (throws ArithmeticException))
; true

Spuštění testů

Midje předpokládá pro spuštění (všech) testů nějaký nástroj, jako např. Leiningen nebo Cake. Pojďme se podívat, jak by vypadalo rozchození projektu a testů pomocí Leiningenu. Nový projekt vytvoříme příkazem:
lein new blog-midje

Struktura projektu by měla vypadat takto:

Layout projektu

Do definice projektu (project.clj) je potřeba přidat závislosti. Kromě těch obligátních je to hlavně knihovna lein-midje:
(defproject blog-midje "1.0.0-SNAPSHOT"
            :description "Midje for Sometimes Clojure"
            :dependencies [[org.clojure/clojure "1.3.0"]]
            :dev-dependencies [[midje "1.3.2-SNAPSHOT"]
                               [lein-midje "1.0.8"]])
Prvně napíšeme test (soubor test/blog_midje/test/core.clj):
(ns blog-midje.test.core
  (:use [blog-midje.core])
  (:use [midje.sweet]))

(tabular
  (fact "The rules of XOR"
        (xor ?x ?y) => ?expected)
  ?x    ?y    ?expected
  0     0     false
  0     1     true
  1     0     true
  1     1     false
  false false false
  false true  true
  true  false true
  true  true  false)
Potom napíšeme testovanou funkci (src/blog_midje/core.clj):
(ns blog-midje.core)

(defn xor
    "Returns true if x and y are mutually exclusive."
    [x y]
    (if (= x y) false true))
Testy potom spustíme příkazem lein test, nebo lépe lein midje:

Spuštění testů

Celý projekt je ke stažení zde: blog-midje.zip.

[Update]Celý projekt je dispozici na Bitbucketu: blog-midje[/Update]

To je pro dnešek vše. Pokud se chcete podívat na úvod do Midje od samotného Briany Maricka, zkuste  videa uvedená na wiki stránce Midje.


15. srpna 2011

Testování v Clojure

Ačkoliv Rich Hickey říká, že nepíše unit testy, přece jenom do Clojure zahrnul "framework" pro unit testy. Já sám, naopak, jsem již léty TDD infikován a testy píšu rád. Takže, jak se to dělá v Clojure? Nejprve natáhneme knihovnu clojure.test:
(ns my-tests (:use clojure.test))
; nil

Aserce (tvrzení)

Základem testů je makro is:
(is (= 2 (+ 1 1)))
; true
(is (odd? 1))
; true
(is (even? 1) "truth about one")
; 
; FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:30)
; truth about one
; expected: (even? 1)
;   actual: (not (even? 1))
; false
Více assertions/parametrů se dá testovat pomocí makra are:
(are [x y] (= 42 (+ x y))
     21 21
     20 22
     42 0)
; true
Vyhození výjimky se dá otestovat pomocí funkce thrown?:
(is (thrown? ArithmeticException (/ 42 0)))
; #<ArithmeticException java.lang.ArithmeticException: Divide by zero>
(is (thrown? ArithmeticException (/ 42 42)))
; 
; FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:48)
; expected: (thrown? ArithmeticException (/ 42 42))
;   actual: nil
; nil

Definice testů

OK, to byly tvrzení (assertions). Jak definuji test? Jedna z možností je makro with-test:
(with-test
    (defn add [x y]
        (+ x y))
    (is (= 42 (add 21 21)))
    (is (= 42 (add 20 22)))
    (is (= 42 (add 42 0))))
Druhou možností je použít makro deftest. Jeho výhodou je, že vytváří funkci (v našem případě ultimate-addition), kterou lze zavolat z libovolného namespace. To umožňuje mít testy v samostatném souboru tak, jak jsme z unit testů zvyklí.
(deftest ultimate-addition
    (is (= 42 (add 21 21)))
    (is (= 42 (add 20 22)))
    (is (= 42 (add 42 0))))

Spuštění testů

Máme definované dva testy (a šest asercí). Jak je spustíme? Testy v aktuálním namespace odpálíme funkcí run-tests:
(run-tests)
;
; Testing my-tests
;
; Ran 2 tests containing 6 assertions.
; 0 failures, 0 errors.
; {:type :summary, :pass 6, :test 2, :error 0, :fail 0}
Testy v jiném namespace spustíme obdobně:
(run-tests 'some.namespace 'some.other.namespace)

Automatizace testů

Tak. Konec srandiček! Velký kluci buildujou (a spouští testy) Leiningenem. Prvně vytvoříme projekt příkazem lein new lein-tests:

Vytvoření a layout projektu

Test first! Napíšeme testy (soubor core.clj v adresáři test):
(ns lein-tests.test.core
  (:use [lein-tests.core])
  (:use [clojure.test]))

(deftest split-line-test
  (is (= ["foo" "bar"]
         (split-line "foo;bar" #";")))
  (is (= ["foo" "bar"]
         (split-line "foo,bar" #", ?")))
  (is (= ["foo" "bar"]
         (split-line "foo, bar" #", ?"))))

(deftest get-phone-test
  (is (= "123456789"
         (get-phone "123456789;Guido")))
  (is (= ""
         (get-phone ";Guido"))))
A napíšeme (testované) funkce (soubor core.clj v adresáři src):
(ns lein-tests.core
  (:use [clojure.string :only (split)]))

(defn split-line [line separator]
  (split line separator))

(defn get-phone [line]
  (first (split-line line #";")))
No a na závěr testy spustíme příkazem lein test. Nádhera!

Spuštění testů

27. ledna 2011

Functions without side effects

Pročítal jsem si na webu nějaké materiály o funkcionálním programování. Jedním ze základních principů je, že funkce nesmí mít vedlejší efekty (side effects). Výčet a definice těchto principů se v různých zdrojích liší, mne konkrétně oslovily tyto:

  • Pokud je funkce volána s parametry, které nezpůsobují vedlejší efekty, její opakované volání (se stejnými parametry) vrací vždy stejný výsledek.
  • Funkce nesmí měnit vstupní parametry a globální proměnné.
  • Funkce nesmí měnit své chování na základě stavů definovaných mimo funkci.
Tyto principy se samozřejmě dají použít v jakémkoli typu programování, tedy i nejrozšířenějším OOP. A pokud se při vývoji používá TDD, nebo aspoň člověk zodpovědně píše unit testy, dojde k těmto principům zcela přirozeně.

Proč vlastně o něčem takovém (možná triviálním) píšu? Dopisoval jsem teď nedávno unit testy k hotové aplikaci a čas od času jsem narazil na kód, který porušoval všechny tři výše uvedené principy zároveň. Takový kód se samozřejmě velice špatně a také velice pracně testuje. Pro představu:

private void checkSMS() {
    if (sms == OperationEnum.ADDED) {
        if (servis24 == OperationEnum.ADDED
                        || servis24 ==
                            OperationEnum.NOT_CHANGED) {
            codeOperationsList.add(OperationCodeEnum
                            .FILING_SMS_WITH_S24
                                .getCode());
        } else {
            codeOperationsList.add(OperationCodeEnum
                            .FILING_SMS_WITHOUT_S24
                                .getCode());
        }
    }
}

Když to vezmu od "spoda":
  • Funkce se rozhoduje na základě (globálních) flagů sms a servis24.
  • Funkce mění globální/instanční proměnnou codeOperationList.
  • Funkce vrací void. Tedy ať už dělá cokoliv, je to side efect.
Předchozí ukázka je samozřejmě z Javy, takže s/funkce/metoda/g.

No a jaké z toho všeho plyne poučení? Pište funkcionální metody! A pište unit testy! Aplikace pak půjde líp otestovat. Lépe spravovat. A po čase snadněji porozumíte kódu.