Obsah
1. Knihovny a moduly usnadňující testování aplikací naprogramovaných v jazyce Clojure
2. Tvorba jednotkových testů s využitím základní knihovny clojure.test
4. Test, zda byla vyhozena výjimka specifikovaného typu nebo výjimka s určitou zprávou
5. Rozšiřující modul humane-test-output pro vylepšení hlášení výstupu jednotkových testů
6. Rozdíly mezi běžným výstupem testů a výstupem upraveným humane-test-output
7. Jednotkové testy s výstupem kompatibilním s JUnit
8. Spuštění jednotkového testu a konverze výsledku do formátu kompatibilního s JUnit
9. Plugin cloverage – zjištění pokrytí kódu testy
10. Příklad výstupu generovaného pluginem cloverage
11. Knihovna iota a její použití při tvorbě testů
12. Nové infixové operátory operátory použité v makru given
13. Ukázka testu napsaného s využitím knihovny iota
14. Testování s využitím knihovny Expectations
15. Testy napsané s využitím knihovny Expectations
16. Podpora pro BDD (behaviour-driven development) knihovnou cucumber-jvm-clojure
17. Jednoduchý projekt testovaný s využitím BDD
18. Repositář s demonstračními příklady
19. Odkazy na předchozí části tohoto seriálu
1. Knihovny a moduly usnadňující testování aplikací naprogramovaných v jazyce Clojure
Programovací jazyk Clojure se dnes může pochlubit poměrně rozsáhlým ekosystémem, ať již se to týká knihoven či různých podpůrných nástrojů. Nezapomnělo se ani na knihovny popř. moduly pro Leiningen určené pro tvorbu testů. V dnešním superdlouhém článku se s některými užitečnými nástroji a knihovnami pro testování seznámíme.
Nejprve si připomeneme možnosti nabízené standardní knihovnou clojure.test, poté si ukážeme, jak je možné vylepšit způsob zobrazení výsledků testů s využitím nástroje humane-test-output. Výsledky testů je možné upravit do podoby kompatibilní s JUnit, což je téma, kterému se budeme věnovat v sedmé kapitole. Následovat bude popis pluginu cloverage, jenž dokáže zobrazit ty řádky kódu, které jsou pokryté testy. Jednodušší psaní jednotkových testů podporuje knihovna iota a na závěr si popíšeme možnosti knihovny Expectations a v neposlední řadě i cucumber-jvm-clojure určené pro psaní BDD testů v jazyku Gherkin.
2. Tvorba jednotkových testů s využitím základní knihovny clojure.test
Tvorbou jednotkových testů s využitím základní knihovny nazvané clojure.test jsme se již v tomto seriálu věnovali, takže se o možnostech této knihovny zmíníme jen krátce. Připomeňme si pouze, že clojure.test, která patří mezi standardní knihovny dodávané společně s interpretrem Clojure, obsahuje několik funkcí a maker určených pro psaní jednotkových testů popř. pro jednoduché aserce zapisované přímo do programového kódu (například při ladění v interaktivní smyčce REPL). Tuto knihovnu navíc automaticky používá systém pro správu projektů Leiningen, takže když vytvoříte strukturu nového projektu příkazem:
lein new app nova_aplikace
objeví se ve struktuře projektu i podadresář test, v němž se vytváří jednotkové testy:
├── doc │ └── intro.md ├── LICENSE ├── project.clj ├── README.md ├── resources ├── src │ └── nova_aplikace │ └── core.clj └── test └── nova_aplikace └── core_test.clj
Moduly uložené v adresáři test knihovnu clojure.test automaticky načítají:
(ns nova-aplikace.core-test (:require [clojure.test :refer :all] [nova-aplikace.core :refer :all])) ... ... ...
Samotný test se zapisuje následujícím způsobem:
(deftest factorial-test (testing "Factorial" (is ( = (factorial 0) 1) "beginning") (is ( = (factorial 1) 1) "beginning") (is ( = (factorial 2) (* 1 2)) "still easy") (is ( = (factorial 5) (* 1 2 3 4 5)) "5!") (is ( = (factorial 6) 720) "6!")))
Můžeme zde vidět použití tří maker nazvaných deftest, testing a is.
Makro deftest sémanticky odpovídá definici funkce, ovšem nikde se neuvádí parametry:
user=> (doc deftest) ------------------------- clojure.test/deftest ([name & body]) Macro Defines a test function with no arguments. Test functions may call other tests, so tests may be composed. If you compose tests, you should also define a function named test-ns-hook; run-tests will call test-ns-hook instead of testing all vars. Note: Actually, the test body goes in the :test metadata on the var, and the real function (the value of the var) calls test-var on itself. When *load-tests* is false, deftest is ignored.
Použití druhého makra testing je nepovinné, ale může se hodit například v případě, že potřebujeme vypisovat informace o tom, jaká část aplikace je právě testována:
user=> (doc testing) ------------------------- clojure.test/testing ([string & body]) Macro Adds a new string to the list of testing contexts. May be nested, but must occur inside a test function (deftest).
Příklad výstupu testu:
lein test Ran 2 tests containing 8 assertions. 0 failures, 0 errors.
Popř. při zjištění chyby nebo chyb:
lein test lein test factorial.core-test lein test :only factorial.core-test/factorial-test FAIL in (factorial-test) (core_test.clj:9) Factorial still easy expected: (= (factorial 2) (* 1 2)) actual: (not (= 1 2)) lein test :only factorial.core-test/factorial-test FAIL in (factorial-test) (core_test.clj:10) Factorial 5! expected: (= (factorial 5) (* 1 2 3 4 5)) actual: (not (= 24 120)) lein test :only factorial.core-test/factorial-test FAIL in (factorial-test) (core_test.clj:11) Factorial 6! expected: (= (factorial 6) 720) actual: (not (= 120 720)) Ran 2 tests containing 8 assertions. 3 failures, 0 errors. Tests failed.
3. Makra is a are
Základem pro psaní jednotkových testů s využitím knihovny clojure.test je makro nazvané jednoduše is, takže se nejprve podívejme na to, co o tomto makru říká dokumentace. K prohlížení dokumentace přímo z interaktivní smyčky REPL slouží makro doc, kterému se jako parametr předá jméno funkce, makra či symbolu, jehož význam potřebujeme zjistit:
user=> (doc is) ------------------------- clojure.test/is ([form] [form msg]) Macro Generic assertion macro. 'form' is any predicate test. 'msg' is an optional message to attach to the assertion. Example: (is (= 4 (+ 2 2)) "Two plus two should be 4") Special forms: (is (thrown? c body)) checks that an instance of c is thrown from body, fails if not; then returns the thing thrown. (is (thrown-with-msg? c re body)) checks that an instance of c is thrown AND that the message on the exception matches (with re-find) the regular expression re. nil
Vidíme, že tomuto makru lze předat takzvaný predikát a popř. i textovou zprávu. Predikát je použit ve dvou významech – po svém vyhodnocení se zjišťuje výsledná hodnota a pokud není predikát splněn, vypíše se chybové hlášení obsahující jak původní znění predikátu, tak i aktuální (odlišnou) hodnotu vzniklou vyhodnocením. Mimochodem: právě proto, že se vypisuje text predikátu, nemůže být is implementováno pomocí funkce, ale bylo nutné použít makro. Chování makra is si můžeme snadno odzkoušet:
user=> (is true) true
user=> (is (= (+ 1 1) 2)) true
user=> (is (= (inc 1) 2)) true
user=> (is (nil? nil)) true
user=> (is (seq? '(1 2 3))) true
user=> (is (fn? println)) true
Co se stane ve chvíli, kdy není predikát splněn, lze opět snadno odzkoušet:
user=> (is (= 1 2)) FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:1) expected: (= 1 2) actual: (not (= 1 2)) false
user=> (is (nil? "ja nejsem nil")) FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:1) expected: (nil? "ja nejsem nil") actual: (not (nil? "ja nejsem nil")) false
user=> (is (= (inc 1) 3)) FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:1) expected: (= (inc 1) 3) actual: (not (= 2 3)) false
user=> (is (fn? true)) FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:1) expected: (fn? true) actual: (not (fn? true)) false
Řádek začínající slovem „FAIL“ jen naznačuje, že makro is spouštíme z interaktivní konzole a nikoli ze zdrojového kódu (kde by bylo známé jak jméno zdrojového souboru, tak i číslo řádku, na němž je makro is použito). Tento nedostatek se nijak neprojeví při testování reálných aplikací.
user=> (assert (= 1 1)) nil user=> (assert (= 1 2)) AssertionError Assert failed: (= 1 2) user/eval771 (NO_SOURCE_FILE:1)
Jedinou vážnější nevýhodou předchozích testů je opakované použití makra is a z toho vyplývající záplavy závorek. Aby se psaní testů zpřehlednilo, lze namísto is využít makro pojmenované are, kterému se předá funkce provádějící porovnání (jen se nezapisuje jméno funkce) a za tímto zápisem pak již většinou seznam obsahující očekávané hodnoty a volání testované funkce:
user=> (doc are) ------------------------- clojure.test/are ([argv expr & args]) Macro Checks multiple assertions with a template expression. See clojure.template/do-template for an explanation of templates. Example: (are [x y] (= x y) 2 (+ 1 1) 4 (* 2 2)) Expands to: (do (is (= 2 (+ 1 1))) (is (= 4 (* 2 2)))) Note: This breaks some reporting features, such as line numbers. nil
Rozdíl mezi makry is a are je patrný z následujících dvou testů:
(ns testing1.core-test (:require [clojure.test :refer :all] [testing1.core :refer :all])) (deftest test-add-1 (testing "function add" (is (= 0 (add 0 0))) (is (= 3 (add 1 2))) (is (= 5/6 (add 1/2 1/3))))) (deftest test-add-2 (testing "function add" (are [x y] (= x y) 0 (add 0 0) 3 (add 1 2) 5/6 (add 1/2 1/3))))
4. Test, zda byla vyhozena výjimka specifikovaného typu nebo výjimka s určitou zprávou
V nápovědě zobrazené k makru is je mj. popsáno i jedno velmi často využívané volání tohoto makra:
(is (thrown? c body))
Tuto formu je v případě potřeby možné použít pro otestování, zda zavolání nějaké funkce vyvolá výjimky určitého typu (typ je určen třídou c). Podívejme se na velmi jednoduchý příklad. Tím je dělení nulou, které podle očekávání vede k vyhození výjimky typu ArithmeticException. Ostatně můžeme se sami přesvědčit, zda je to pravda:
user=> (/ 42 0) ArithmeticException Divide by zero clojure.lang.Numbers.divide (Numbers.java:156)
Výjimka skutečně byla podle všech očekávání vyhozena, takže můžeme zkusit, co se stane ve chvíli, kdy se využije výše uvedená speciální forma volání makra is:
user=> (is (thrown? ArithmeticException (/ 42 0))) #<ArithmeticException java.lang.ArithmeticException: Divide by zero<
Výsledkem volání je instance třídy ArithmeticException. Opět se můžeme snadno přesvědčit, že je to pravda:
user=> (def result (is (thrown? ArithmeticException (/ 42 0)))) #'user/result user=> result #<ArithmeticException java.lang.ArithmeticException: Divide by zero< user=> (type result) java.lang.ArithmeticException
Ve chvíli, kdy se použije format (is (thrown? …)) a k vyhození výjimky nedojde, vypíše makro is následující zprávu:
user=> (is (thrown? ArithmeticException (/ 42 1))) FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:1) expected: (thrown? ArithmeticException (/ 42 1)) actual: nil nil
V některých případech může být důležité otestovat nejenom typ výjimky, ale i to, jestli zpráva nesená výjimkou odpovídá zadanému regulárnímu výrazu. Připomeňme si, že v Clojure se regulární výrazy zapisují ve formátu #„regulární_výraz“, díky čemuž je možné se vyhnout nutnosti escapování mnoha znaků, které mají v regulárních výrazech speciální význam. Podívejme se na následující test, který zjistí, jestli výjimka obsahuje zprávu „No such file or directory“ (což platí pro Linux, ne nutně pro další systémy):
user=> (is (thrown-with-msg? java.io.FileNotFoundException #"No such file or directory" (slurp "nejaky_soubor"))) #<FileNotFoundException java.io.FileNotFoundException: nejaky_soubor (No such file or directory)>
Naopak test, jehož podmínka není splněna:
user=> (is (thrown-with-msg? java.io.FileNotFoundException #"Soubor nelze najit" (slurp "nejaky_soubor"))) FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:1) expected: (thrown-with-msg? java.io.FileNotFoundException #"Soubor nelze najit" (slurp "nejaky_soubor")) actual: #<FileNotFoundException java.io.FileNotFoundException: nejaky_soubor (No such file or directory)> #<FileNotFoundException java.io.FileNotFoundException: nejaky_soubor (No such file or directory)>
Pro testování zprávy se interně volá funkce re-find, takže pokud potřebujete zjistit, zda celá zpráva odpovídá zadanému regulárnímu výrazu, je nutné na začátku a konci výrazu použít znaky ^ a $:
user=> (is (thrown-with-msg? java.lang.ArrayIndexOutOfBoundsException #"^[0-9]+$" (aget (int-array 10) 100))) #<ArrayIndexOutOfBoundsException java.lang.ArrayIndexOutOfBoundsException: 100> user=> (is (thrown-with-msg? java.lang.ArrayIndexOutOfBoundsException #"^[0-9]+$" (aget (int-array 10) -1))) FAIL in clojure.lang.PersistentList$EmptyList@1 (NO_SOURCE_FILE:1) expected: (thrown-with-msg? java.lang.ArrayIndexOutOfBoundsException #"^[0-9]+$" (aget (int-array 10) -1)) actual: #<ArrayIndexOutOfBoundsException java.lang.ArrayIndexOutOfBoundsException: -1> #<ArrayIndexOutOfBoundsException java.lang.ArrayIndexOutOfBoundsException: -1>
5. Rozšiřující modul humane-test-output pro vylepšení hlášení výstupu jednotkových testů
Z předchozích příkladů je patrné, že výstup produkovaný výše popsanými makry is a are (popř. i dalšími makry a funkcemi poskytovanými knihovnou clojure.test) nemusí být příliš čitelný. Tento problém se stane ještě více patrný ve chvíli, kdy například testujeme, zda se vrátil určitý řetězec a chyba (přesněji řečeno rozdíl) nastane v jediném znaku. Podobně se může stát, že porovnáváme dvě rozsáhlejší či složitější datové struktury, které se opět mohou odlišovat pouze v několika detailech. Klasický výstup produkovaný makry is a are v těchto případech bude vypadat následovně:
user=> (use '[clojure.test]) nil user=> (is (= "Linux was originally developed for personal computers based on the Intel x86 architecture" "Linux was originally developed for personal computers based on the Intel i386 architecture")) FAIL in () (form-init2657361174634375861.clj:1) expected: (= "Linux was originally developed for personal computers based on the Intel x86 architecture" "Linux was originally developed for personal computers based on the Intel i386 architecture") actual: (not (= "Linux was originally developed for personal computers based on the Intel x86 architecture" "Linux was originally developed for personal computers based on the Intel i386 architecture")) false
Stručně a přitom nečitelně se napíše, že dva porovnávané řetězce jsou rozdílné.
Pro dosažení čitelnějších výsledků testů je možné použít několik různých modulů. Pravděpodobně nejužitečnější je modul nazvaný přímočaře humane-test-output. Tento modul je nejdříve nutné nainstalovat, a to ideálně pro celé vývojové prostředí programátora. Provede se to poměrně snadnou úpravou souboru profiles.clj, který se nachází v adresáři ~/.lein (~ nahrazuje domovský adresář uživatele):
{:user {:dependencies [[pjstadig/humane-test-output "0.8.3"]] :injections [(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)]}}
Pokud nyní znovu spustíme REPL a porovnáme dva řetězce pomocí makra is, tak se chybové hlášení změní do čitelnější podoby. Navíc se kromě dvou porovnávaných hodnot vypíše i jejich rozdíl (což ovšem v případě řetězců nedává moc velký význam; pravý smysl uvidíme později):
user=> (use '[clojure.test]) nil user=> (is (= "Linux was originally developed for personal computers based on the Intel x86 architecture" "Linux was originally developed for personal computers based on the Intel i386 architecture")) FAIL in () (form-init7785123977273257765.clj:1) expected: "Linux was originally developed for personal computers based on the Intel x86 architecture" actual: "Linux was originally developed for personal computers based on the Intel i386 architecture" diff: - "Linux was originally developed for personal computers based on the Intel x86 architecture" + "Linux was originally developed for personal computers based on the Intel i386 architecture" false
Další příklad použití – porovnání dvou delších sekvencí bez přídavného modulu:
user=> (is (= (range 10) (range 11))) FAIL in () (form-init7847812831457116868.clj:1) expected: (= (range 10) (range 11)) actual: (not (= (0 1 2 3 4 5 6 7 8 9) (0 1 2 3 4 5 6 7 8 9 10))) false
Totéž porovnání, nyní ovšem se zapnutým modulem humane-test-output:
user=> (is (= (range 10) (range 11))) FAIL in () (form-init2139708971885779974.clj:1) expected: (0 1 2 3 4 5 6 7 8 9) actual: (0 1 2 3 4 5 6 7 8 9 10) diff: + [nil nil nil nil nil nil nil nil nil nil 10] false
Porovnání dvou map, které se od sebe liší jedinou hodnotou:
user=> (is (= {:a 1 :b 2 :c 3} {:a 1 :b 3 :c 3})) FAIL in () (form-init7847812831457116868.clj:1) expected: (= {:a 1, :b 2, :c 3} {:a 1, :b 3, :c 3}) actual: (not (= {:a 1, :b 2, :c 3} {:a 1, :b 3, :c 3})) false
Totéž porovnání, nyní ovšem se zapnutým modulem humane-test-output:
user=> (is (= {:a 1 :b 2 :c 3} {:a 1 :b 3 :c 3})) FAIL in () (form-init2139708971885779974.clj:1) expected: {:a 1, :b 2, :c 3} actual: {:a 1, :b 3, :c 3} diff: - {:b 2} + {:b 3} false
Zejména v posledním případě jistě oceníte mnohem vyšší čitelnost výstupu.
6. Rozdíly mezi běžným výstupem testů a výstupem upraveným humane-test-output
Podívejme se nyní na to, jak se výstup produkovaný jednotkovými testy změní ve chvíli, kdy je nakonfigurován modul humane-test-output a provádíme testování nějaké aplikace příkazem lein test. Nejdříve vyzkoušíme naši funkci pro výpočet faktoriálu, přesněji řečeno takovou variantu funkce factorial, která vyhazuje výjimku pro záporná čísla:
(ns factorial4.core (:gen-class)) ; funkce faktorial obsahuje i test na zaporne hodnoty (defn factorial [n] (if (neg? n) (throw (IllegalArgumentException. "negative numbers are not supported!")) (apply * (range 1 (inc n))))) ; otestujeme funkci faktorial (defn -main [& args] (doseq [i (range 0 10)] (println i "! = " (factorial i))))
Samotné testy jsou naprogramovány pro odzkoušení známých výsledků i pro otestování, kdy dojde k chybě nebo dokonce k vyhození výjimky. Zvýrazněný test je schválně napsán nekorektně, aby při jeho spuštění byly hlášeny špatné výsledky:
(ns factorial4.core-test (:require [clojure.test :refer :all] [factorial4.core :refer :all])) (deftest factorial-test (testing "Factorial" (is ( = (factorial 0) 1) "beginning") (is ( = (factorial 1) 1) "beginning") (is ( = (factorial 2) (* 1 2)) "still easy") (is ( = (factorial 5) (* 1 2 3 4 5)) "5!") (is ( = (factorial 6) 720) "6!"))) (deftest negative-factorial-test (testing "Negative tests" (is ( = (factorial 0) 0) "negative test case #1") (is ( = (factorial 1) 0) "negative test case #2") (is ( = (factorial 2) 0) "negative test case #3"))) (deftest exception-test (testing "If factorial throws exception" (is (thrown? IllegalArgumentException (factorial -1))) (is (thrown? IllegalArgumentException (factorial -2))) (is (thrown? IllegalArgumentException (factorial -100))))) (deftest negative-exception-test (testing "(negative test) If factorial throws exception" (is (thrown? IllegalArgumentException (factorial 1))) (is (thrown? IllegalArgumentException (factorial 2))) (is (thrown? IllegalArgumentException (factorial 3)))))
V případě, že se nepoužije modul humane-test-output, bude vygenerovaný výstup obsahovat mj. i zvýrazněné řádky, které pouze opakují krok testu a také fakt, že se výsledky neshodují s očekávanými hodnotami (not (= 1 0)):
lein test factorial4.core-test lein test :only factorial4.core-test/negative-factorial-test FAIL in (negative-factorial-test) (core_test.clj:15) Negative tests negative test case #1 expected: (= (factorial 0) 0) actual: (not (= 1 0)) lein test :only factorial4.core-test/negative-factorial-test FAIL in (negative-factorial-test) (core_test.clj:16) Negative tests negative test case #2 expected: (= (factorial 1) 0) actual: (not (= 1 0)) lein test :only factorial4.core-test/negative-factorial-test FAIL in (negative-factorial-test) (core_test.clj:17) Negative tests negative test case #3 expected: (= (factorial 2) 0) actual: (not (= 2 0)) lein test :only factorial4.core-test/negative-exception-test FAIL in (negative-exception-test) (core_test.clj:27) (negative test) If factorial throws exception expected: (thrown? IllegalArgumentException (factorial 1)) actual: nil lein test :only factorial4.core-test/negative-exception-test FAIL in (negative-exception-test) (core_test.clj:28) (negative test) If factorial throws exception expected: (thrown? IllegalArgumentException (factorial 2)) actual: nil lein test :only factorial4.core-test/negative-exception-test FAIL in (negative-exception-test) (core_test.clj:29) (negative test) If factorial throws exception expected: (thrown? IllegalArgumentException (factorial 3)) actual: nil Ran 4 tests containing 14 assertions. 6 failures, 0 errors.
V případě, kdy naopak modul humane-test-output korektně nakonfigurujeme a použijeme, budou výsledky mnohem čitelnější, což je ostatně patrné z následujícího výpisu:
lein test factorial4.core-test lein test :only factorial4.core-test/negative-factorial-test FAIL in (negative-factorial-test) (core_test.clj:15) Negative tests negative test case #1 expected: 1 actual: 0 diff: - 1 + 0 lein test :only factorial4.core-test/negative-factorial-test FAIL in (negative-factorial-test) (core_test.clj:16) Negative tests negative test case #2 expected: 1 actual: 0 diff: - 1 + 0 lein test :only factorial4.core-test/negative-factorial-test FAIL in (negative-factorial-test) (core_test.clj:17) Negative tests negative test case #3 expected: 2 actual: 0 diff: - 2 + 0 lein test :only factorial4.core-test/negative-exception-test FAIL in (negative-exception-test) (core_test.clj:27) (negative test) If factorial throws exception expected: (thrown? IllegalArgumentException (factorial 1)) actual: nil lein test :only factorial4.core-test/negative-exception-test FAIL in (negative-exception-test) (core_test.clj:28) (negative test) If factorial throws exception expected: (thrown? IllegalArgumentException (factorial 2)) actual: nil lein test :only factorial4.core-test/negative-exception-test FAIL in (negative-exception-test) (core_test.clj:29) (negative test) If factorial throws exception expected: (thrown? IllegalArgumentException (factorial 3)) actual: nil Ran 4 tests containing 14 assertions. 6 failures, 0 errors.
Zkusme si vytvořit a otestovat ještě jeden příklad, tentokrát napsaný takovým způsobem, aby se porovnávala relativně složitá datová struktura. V následujícím úryvku kódu se vytváří struktura odpovědi (response), kterou server odesílá klientovi. Struktura je před odesláním zpracována knihovnou Ring, tuto část kódu ovšem netestujeme – zajímá nás jen, zda bude datová struktura vytvořená funkcí nazvanou generate-response korektní. Nejprve se podívejme na zdrojový kód testované aplikace (resp. přesněji řečeno její relevantní části):
(ns humane-output.core (:gen-class)) (require '[ring.util.response :as response]) (defn cache-control-headers "Update the response to contains all cache-control headers." [response] (-> response (assoc-in [:headers "Cache-Control"] ["must-revalidate" "no-cache" "no-store"]) (assoc-in [:headers "Expires"] "0") (assoc-in [:headers "Pragma"] "no-cache"))) (defn generate-response [content] (-> (response/response content) (response/content-type "text/plain; charset=utf-8") cache-control-headers)) (defn -main [& args] (println (generate-response "Hello world!")))
Test je napsán velmi jednoduše. Nejprve se v něm definuje očekávaná hodnota (viz symbol expected-response) a následně jen zjistíme, zda je hodnota (tj. celá datová struktura) vrácená funkcí generate-response shodná s očekávanou hodnotou. Připomeňme si, že porovnání funkcí = lze v Clojure provést i pro libovolně složité struktury (o samotný rekurzivní sestup se nemusíme starat):
user=> (doc =) ------------------------- clojure.core/= ([x] [x y] [x y & more]) Equality. Returns true if x equals y, false if not. Same as Java x.equals(y) except it also works for nil, and compares numbers and collections in a type-independent manner. Clojure's immutable data structures define equals() (and thus =) as a value, not an identity, comparison. nil
Povšimněte si, že v testu očekáváme, že pod klíčem Expires je uložena hodnota „-1“ a nikoli „0“. Z tohoto důvodu se po spuštění testů příkazem lein test nahlásí chyba:
(ns humane-output.core-test (:require [clojure.test :refer :all] [humane-output.core :refer :all])) (def expected-response {:status 200, :headers {"Content-Type" "text/plain; charset=utf-8", "Cache-Control" ["must-revalidate" "no-cache" "no-store"], "Expires" "-1", "Pragma" "no-cache"}, :body "hello world!"}) (deftest test-generate-response (testing "Function generate-response" (is (= expected-response (generate-response "hello world!")))))
V případě, že se nepoužije modul humane-test-output, bude výstup dosti nečitelný. Ostatně posuďte sami, kolik času je nutné pro zjištění skutečného problému na řádku actual::
lein test humane-output.core-test lein test :only humane-output.core-test/test-generate-response FAIL in (test-generate-response) (core_test.clj:18) Function generate-response expected: (= expected-response (generate-response "hello world!")) actual: (not (= {:status 200, :headers {"Content-Type" "text/plain; charset=utf-8","Cache-Control" ["must-revalidate" "no-cache" "no-store"], "Expires" "-1", "Pragma" "no-cache"}, :body "hello world!"} {:status 200, :headers {"Content-Type" "text/plain; charset=utf-8", "Cache-Control" ["must-revalidate" "no-cache" "no-store"], "Expires" "0", "Pragma" "no-cache"}, :body "hello world!"})) Ran 1 tests containing 1 assertions. 1 failures, 0 errors.
Naproti tomu je při použití modulu humane-test-output ihned patrné, kde spočívá příčina pádu testu. Obě struktury jsou totiž nejdříve vypsány pod sebou s využitím funkce pprint a navíc se ještě zobrazí pouze rozdíly mezi oběma strukturami formou inteligentního diffu (viz zvýrazněné řádky):
lein test humane-output.core-test lein test :only humane-output.core-test/test-generate-response FAIL in (test-generate-response) (core_test.clj:18) Function generate-response expected: {:status 200, :headers {"Content-Type" "text/plain; charset=utf-8", "Cache-Control" ["must-revalidate" "no-cache" "no-store"], "Expires" "-1", "Pragma" "no-cache"}, :body "hello world!"} actual: {:status 200, :headers {"Content-Type" "text/plain; charset=utf-8", "Cache-Control" ["must-revalidate" "no-cache" "no-store"], "Expires" "0", "Pragma" "no-cache"}, :body "hello world!"} diff: - {:headers {"Expires" "-1"}} + {:headers {"Expires" "0"}} Ran 1 tests containing 1 assertions. 1 failures, 0 errors.
Funkci tohoto modulu je možné kdykoli zakázat nastavením proměnné prostředí INHUMANE_TEST_OUTPUT (to lze provést i z IDE atd.).
7. Jednotkové testy s výstupem kompatibilním s JUnit
Příkazem lein test je sice možné spustit jednotkové testy a získat čitelný výstup, tj. informaci o tom, kolik testů bylo spuštěno, kolik testů proběhlo v pořádku a které testy naopak našly v aplikaci chybu, ovšem výstupní formát je poněkud neobvyklý. Ve světě Javy (a vlastně i mimo tento svět) se ustálilo použití XML formátu kompatibilního s nástrojem JUnit. Tento formát dokážou zpracovat jak mnohá integrovaná vývojová prostředí, tak i například několik přídavných modulů pro systém Jenkins popř. starší Hudson.
Tyto moduly dokážou například vytvářet grafy s regresemi atd., takže by bylo vhodné nějakým způsobem upravit Leiningen takovým způsobem, aby formát JUnitu podporoval. To je samozřejmě možné, a to především díky velké rozšiřitelnosti Leiningenu o další moduly. Modul, který budeme potřebovat, se jmenuje jednoduše test2junit a v následujících odstavcích si ukážeme jeho základní použití.
Aby bylo možné tento modul použít, je nutné upravit projektový soubor project.clj, přesněji řečeno do něj doplnit informaci o používaném pluginu (nikoli knihovně!, ty se totiž zapisují do sekce :dependencies a nikoli :plugins):
(defproject factorial2 "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.6.0"]] :main ^:skip-aot factorial2.core :target-path "target/%s" :plugins [[test2junit "1.1.0"]] :profiles {:uberjar {:aot :all}})
Obrázek 1: Aplikace psané v Clojure testované v CI (Jenkinsu). Díky použití test2junit lze využívat všech pluginů Jenkinsu pro zpracování výsledků testů – viz grafy úspěšnosti.
Následně je vhodné spustit následující příkaz, který zajistí stažení nového modulu a popř. i všech knihoven, na nichž tento modul závisí (pokud tento příkaz nespustíte, spustí se automaticky při prvním volání testů):
lein deps Retrieving test2junit/test2junit/1.1.0/test2junit-1.1.0.pom from clojars Retrieving test2junit/test2junit/1.1.0/test2junit-1.1.0.jar from clojars
Obrázek 2: Tabulka s výsledky jednotkových testů pro aplikace psané v jazyku Clojure.
8. Spuštění jednotkového testu a konverze výsledku do formátu kompatibilního s JUnit
Nyní nastává zajímavý okamžik – spustíme totiž nástroj Leiningen s novým jménem úkolu (task). To je možné, protože Leiningen byl díky úpravě souboru project.clj rozšířen o novou funkcionalitu (onen zmíněný plugin):
lein test2junit
Na standardní výstup se vypíšou následující informace (v některých případech se však ještě stáhnou zbývající knihovny, na nichž dokončení zvoleného úkolu závisí):
Using test2junit version: 1.1.0 Running Tests... Writing output to: test2junit Creating default build.xml file. Testing: factorial2.core-test Ran 2 tests containing 8 assertions. 0 failures, 0 errors.
Výsledkem běhu tohoto nové úlohy (tasku) je soubor build.xml a především pak adresářová struktura test2unit obsahující soubor s cestou test2unit/xml/factorial2.core-test.xml. Podívejme se na obsah tohoto souboru:
<?xml version="1.0" encoding="UTF-8"?> <testsuite name="factorial2.core-test" errors="0" failures="0" tests="2" time="0.0142" timestamp="2015-02-21_20:56:31+0100"> <testcase name="factorial-test" classname="factorial2.core-test" time="0.0044"> </testcase> <testcase name="exception-test" classname="factorial2.core-test" time="0.0013"> </testcase> </testsuite>
Vidíme, že jsou zde uloženy informace jak o jménu spuštěných testů, tak i o době běhu a čase spuštění.
Obrázek 3: Výsledky běhu jednotkových testů pro vybraný projekt naprogramovaný v Clojure.
Pokud uděláme ve zdrojovém kódu aplikace záměrnou chybu – vynechání volání funkce inc – bude výsledek běhu testů odlišný:
lein test2junit Using test2junit version: 1.1.0 Running Tests... Writing output to: test2junit Testing: factorial2.core-test Ran 2 tests containing 8 assertions. 3 failures, 0 errors. Tests failed. Tests failed.
A odlišovat se samozřejmě bude i výstupní XML soubor:o
<?xml version="1.0" encoding="UTF-8"?> <testsuite name="factorial2.core-test" errors="0" failures="3" tests="2" time="0.0232" timestamp="2015-02-21_20:59:03+0100"> <testcase name="factorial-test" classname="factorial2.core-test" time="0.0168"> <failure message="still easy">still easy expected: (= (factorial 2) (* 1 2)) actual: (not (= 1 2)) at: AFn.java:18</failure> <failure message="5!">5! expected: (= (factorial 5) (* 1 2 3 4 5)) actual: (not (= 24 120)) at: AFn.java:18</failure> <failure message="6!">6! expected: (= (factorial 6) 720) actual: (not (= 120 720)) at: AFn.java:18</failure> </testcase> <testcase name="exception-test" classname="factorial2.core-test" time="0.0011"> </testcase> </testsuite>
Obrázek 4: Podrobnější informace o výsledcích jednotkových testů, opět získané díky pluginu test2unit.
9. Plugin cloverage – zjištění pokrytí kódu testy
Dalším užitečným pluginem pro nástroj Leiningen, který je určený pro usnadnění práce testerů či devops, je modul nazvaný cloverage. Úkolem tohoto modulu je zjištění, které části programového kódu jsou pokryté testy, tj. opět se jedná o analogii k podobným nástrojům existujícím i pro další programovací jazyky, ovšem s tím rozdílem, že kvůli použití maker je zjištění pokrytí testy v programovacím jazyku Clojure složitější.
Funkci tohoto pluginu otestujeme jednoduše – použijeme upravený projekt pro výpočet faktoriálu, do nějž jsou přidány další dvě totožné funkce, které se od sebe odlišují pouze jménem:
(ns cloverage.core (:gen-class)) (defn factorial [n] (if (neg? n) (throw (IllegalArgumentException. "negative numbers are not supported!")) (apply * (range 1 (inc n))))) (defn factorial2 [n] (if (neg? n) (throw (IllegalArgumentException. "negative numbers are not supported!")) (apply * (range 1 (inc n))))) (defn factorial3 [n] (if (neg? n) (throw (IllegalArgumentException. "negative numbers are not supported!")) (apply * (range 1 (inc n))))) (defn -main "I don't do a whole lot ... yet." [& args] (doseq [i (range 0 10)] (println i "! = " (factorial i))))
Projektový soubor project.clj musí vypadat následovně (opět si povšimněte nového pluginu na zvýrazněném řádku):
(defproject cloverage "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.6.0"]] :main ^:skip-aot cloverage.core :target-path "target/%s" :plugins [[lein-cloverage "1.0.2"]] :profiles {:uberjar {:aot :all}})
Po přidání tohoto pluginu se při spuštění projektového manažeru lein zobrazí nová úloha:
Leiningen is a tool for working with Clojure projects. Several tasks are available: change Rewrite project.clj by applying a function. check Check syntax and warn on reflection. classpath Print the classpath of the current project. clean Remove all files from project's target-path. cloverage Run code coverage on the project. compile Compile Clojure source into .class files. deploy Build and deploy jar to remote repository. deps Download all dependencies. do Higher-order task to perform other tasks in succession. help Display a list of tasks or help for a given task. install Install the current project to the local repository. jar Package up all the project's files into a jar file. javac Compile Java source files. new Generate project scaffolding based on a template. plugin DEPRECATED. Please use the :user profile instead. pom Write a pom.xml file to disk for Maven interoperability. release Perform :release-tasks. repl Start a repl session either with the current project or standalone. retest Run only the test namespaces which failed last time around. run Run a -main function with optional command-line arguments. search Search remote maven repositories for matching jars. show-profiles List all available profiles or display one if given an argument. test Run the project's tests. trampoline Run a task without nesting the project's JVM inside Leiningen's. uberjar Package up the project files and dependencies into a jar file. update-in Perform arbitrary transformations on your project map. upgrade Upgrade Leiningen to specified version or latest stable. vcs Interact with the version control system. version Print version for Leiningen and the current JVM. with-profile Apply the given task with the profile(s) specified.
Nejzajímavější jsou jednotkové testy. Povšimněte si, že funkce factorial1 je otestována celá, tj. včetně obou větví, funkce factorial2 je otestována jen částečně (pouze jedna větev) a nakonec funkce factorial3 není otestována vůbec. Tyto rozdíly by se nějakým způsobem měly projevit ve výsledcích:
(ns cloverage.core-test (:require [clojure.test :refer :all] [cloverage.core :refer :all])) (deftest factorial-test (testing "Factorial" (is ( = (factorial 0) 1) "beginning") (is ( = (factorial 1) 1) "beginning") (is ( = (factorial 2) (* 1 2)) "still easy") (is ( = (factorial 5) (* 1 2 3 4 5)) "5!") (is ( = (factorial 6) 720) "6!"))) (deftest factorial2-test (testing "Factorial" (is ( = (factorial2 0) 1) "beginning") (is ( = (factorial2 1) 1) "beginning") (is ( = (factorial2 2) (* 1 2)) "still easy") (is ( = (factorial2 5) (* 1 2 3 4 5)) "5!") (is ( = (factorial2 6) 720) "6!"))) (deftest exception-test (testing "If factorial throws exception" (is (thrown? IllegalArgumentException (factorial -1))) (is (thrown? IllegalArgumentException (factorial -2))) (is (thrown? IllegalArgumentException (factorial -100)))))
10. Příklad výstupu generovaného pluginem cloverage
Po přípravě projektového souboru i testů je nutné spustit novou úlohu. Nejedná se o lein test, ale o lein cloverage:
lein cloverage Loading namespaces: (cloverage.core) Test namespaces: (cloverage.core-test) Loaded cloverage.core . Instrumented namespaces. Testing cloverage.core-test Ran 3 tests containing 13 assertions. 0 failures, 0 errors. Ran tests. Produced output in /home/tester/repos/clojure-examples/cloverage/target/coverage . HTML: file:///home/tester/repos/clojure-examples/cloverage/target/coverage/index.html
Kromě HTML výstupu se mj. vygeneruje i následující tabulka:
|----------------+---------+---------| | Namespace | % Forms | % Lines | |----------------+---------+---------| | cloverage.core | 33.33 | 62.50 | |----------------+---------+---------| | ALL FILES | 33.33 | 62.50 | |----------------+---------+---------|
Podívejme se nyní na výsledky – zdá se, že skutečně odpovídají testům:
Obrázek 5: Pokrytí zdrojového kódu testy – celková statistika.
Obrázek 6: Pokrytí zdrojového kódu testy – zelené řádky byly vyhodnoceny, červené nikoli (bílé řádky nepředstavují zdrojový kód).
11. Knihovna iota a její použití při tvorbě testů
Další užitečnou knihovnou usnadňující psaní jednotkových testů je knihovna nazvaná iota. Tato knihovna programátorům nabízí makra usnadňující psaní testů s využitím nových „operátorů“ a navíc bez nutnosti použití velkého množství závorek – ve skutečnosti je možné dosáhnout, že celý jeden test bude zapsán bez jediné závorky. Nejdříve si připravíme nový projekt s touto knihovnou a následně si vyzkoušíme její možnosti v interaktivní smyčce REPL. Do projektového souboru je nutné přidat zvýrazněný řádek:
(defproject iota-test "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.9.0"] [juxt/iota "0.2.3"]] :main ^:skip-aot iota-test.core :target-path "target/%s" :profiles {:uberjar {:aot :all}})
V adresáři s projektem spustíme smyčku REPL:
lein repl
Následně musíme načíst jak knihovnu clojure.test, tak i makro given z knihovny iota:
user=> (use '[clojure.test]) nil user=> (require '[juxt.iota :refer [given]])
Popis nově načteného makra given je dosti stručný, ale to nevadí, protože si jeho schopnosti ukážeme o několik odstavců níže:
user=> (doc given) ------------------------- juxt.iota/given ([v & body]) Macro Given v, assert the following… nil
12. Nové infixové operátory operátory použité v makru given
Makro given ve svém prvním parametru očekává libovolnou hodnotu či funkci vracející hodnotu. Následuje libovolně dlouhá sekvence podmínek, přičemž každá podmínka je zapisovaná v „lidském“ tvaru selektor operátor očekávaná_hodnota. Podívejme se na příklad, v němž kontrolujeme, jaké hodnoty jsou uloženy v mapě. Pro striktní porovnání hodnot slouží nový infixový operátor :=. Zapsány jsou celkem dvě podmínky, a to pro selektor :a a :b:
user=> (given {:a 1 :b 2} :a := 1 :b := 2) true user=> (given {:a 1 :b 2} :a := 1 :b := 1) FAIL in () (form-init1562388683681536377.clj:1) expected: (clojure.core/= ((juxt.iota/as-test-function :b) G__2883) 1) actual: (not (clojure.core/= 2 1)) false
Pomocí operátoru :? lze zjistit, zda hodnota vybraná selektorem odpovídá zvolenému predikátu; druhý operátor :!? testuje opačnou podmínku. Predikáty již dobře známe, takže se podívejme na příklady:
user=> (given {:a 1 :b 0 :c [1,2,3]} :a :? pos-int? :b :? zero? :c :? vector?) true user=> (given {:a 1 :b 0 :c [1,2,3]} :a :!? neg-int? :b :!? pos-int? :c :!? seq?) true user=> (given {:a 1 :b 0 :c [1,2,3]} :a :!? neg-int? :b :!? zero? :c :!? seq?) FAIL in () (form-init1562388683681536377.clj:1) expected: (clojure.core/not (zero? ((juxt.iota/as-test-function :b) G__2949))) actual: (not (clojure.core/not true)) false
Operátorem :< se zjišťuje, zda je vybraná hodnota podmnožinou druhého operandu. Ovšem ve skutečnosti se i sekvence a vektory převádí na množiny, takže lze psát:
user=> (given {:c [1 2 3]} :c :< [1 2 3 4 5]) true user=> (given {:c [1 2 3 10]} :c :< (range 10)) FAIL in () (form-init1562388683681536377.clj:1) expected: (clojure.set/subset? (clojure.core/set ((juxt.iota/as-test-function :c) G__2977)) (clojure.core/set (range 10))) actual: (not (clojure.set/subset? #{1 3 2 10} #{0 7 1 4 6 3 2 9 5 8})) false
Tento operátor je možné zapisovat i příslušným Unicode znakem:
user=> (given {:c [1 2 3]} :c :< (range 10)) true user=> (given {:c [1 2 3 10]} :c :⊂ (range 10)) FAIL in () (form-init1562388683681536377.clj:1) expected: (clojure.set/subset? (clojure.core/set ((juxt.iota/as-test-function :c) G__2992)) (clojure.core/set (range 10))) actual: (not (clojure.set/subset? #{1 3 2 10} #{0 7 1 4 6 3 2 9 5 8})) false
Opačnou podmínku, tj. zda je druhý operand podmnožinou operandu prvního, můžeme taktéž zapsat dvěma způsoby:
user=> (given {:c [1 2 3 10]} :c :> []) true user=> (given {:c [1 2 3 10]} :c :⊃ []) true
Posledním operátorem, o kterém se dnes ve stručnosti zmíníme, je operátor :#, který testuje hodnotu vůči regulárnímu výrazu:
user=> (given {:body "Hello world"} :body :# #"[a-z]+") FAIL in () (form-init1562388683681536377.clj:1) expected: (clojure.core/re-matches (clojure.core/re-pattern #"[a-z]+") ((juxt.iota/as-test-function :body) G__2997)) actual: (not (clojure.core/re-matches #"[a-z]+" "Hello world")) nil user=> (given {:body "hello world"} :body :# #"[a-z]+") FAIL in () (form-init1562388683681536377.clj:1) expected: (clojure.core/re-matches (clojure.core/re-pattern #"[a-z]+") ((juxt.iota/as-test-function :body) G__3002)) actual: (not (clojure.core/re-matches #"[a-z]+" "hello world")) nil user=> (given {:body "hello world"} :body :# #"[a-z ]+") "hello world"
13. Ukázka testu napsaného s využitím knihovny iota
Zkusme si nyní vytvořit jednoduchý projekt, v němž knihovnu iota použijeme. Projektový soubor project.clj bude vypadat následovně:
(defproject iota-test "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.9.0"] [juxt/iota "0.2.3"] [ring/ring-core "1.3.2"]] :main ^:skip-aot iota-test.core :target-path "target/%s" :profiles {:uberjar {:aot :all}})
Samotný zdrojový kód bude opět obsahovat funkci generate-response, která vytvoří mapu představující odpověď serveru posílanou klientovi:
(ns iota-test.core (:gen-class)) (require '[ring.util.response :as response]) (defn cache-control-headers "Update the response to contains all cache-control headers." [response] (-> response (assoc-in [:headers "Cache-Control"] ["must-revalidate" "no-cache" "no-store"]) (assoc-in [:headers "Expires"] "0") (assoc-in [:headers "Pragma"] "no-cache"))) (defn generate-response [content] (-> (response/response content) (response/content-type "text/plain; charset=utf-8") cache-control-headers)) (defn -main [& args] (println (generate-response "Hello world!")))
Nejzajímavější jsou samozřejmě testy, protože zde můžeme využít nové operátory. Povšimněte si, že hodnotu uloženou pod klíčem :status testujeme na rovnost, dále pro testy používáme predikát string?, test s využitím regulárního výrazu a v neposlední řadě i test na relaci „je nadmnožinou“. Dále si povšimněte, že složitější selektor lze zapsat formou vektoru:
(ns iota-test.core-test (:require [clojure.test :refer :all] [iota-test.core :refer :all] [juxt.iota :refer [given]])) (deftest test-generate-response (testing "Function generate-response" (given (generate-response "hello world!") :status := 200 :body :# #"[a-zA-Z !]+" [:headers "Pragma"] :? string? [:headers "Content-Type"] :? string? [:headers "Content-Type"] :? #(.startsWith % "text/plain; ") [:headers "Cache-Control"] :> ["no-cache" "no-store"])))
Samotné spuštění testů se provede standardním způsobem:
lein test iota-test.core-test Ran 1 tests containing 6 assertions. 0 failures, 0 errors.
14. Testování s využitím knihovny Expectations
Nízkoúrovňový přístup knihovny clojure.test sice do určité míry vylepšuje výše zmíněná knihovna iota, ovšem k dispozici jsou i další možnosti. Poněkud odlišným směrem se vydal vývojář Jay Fields, který naprogramoval nástroj nazvaný Expectations (resp. expectations, tedy s malým „e“ na začátku názvu). Tento nástroj sice interně využívá výše zmíněnou knihovnu clojure.test, ovšem staví nad ní mezivrstvu zajišťující rozhraní pro psaní přehlednějších jednotkových testů. V této mezivrstvě nalezneme „inteligentní“ makro expect, které samo o sobě postačuje pro napsání většiny testů. Pro další zjednodušení jsou v nástroji Expectations dostupná i další makra, především pak makro more->, more-of a v neposlední řadě taktéž from-each.
To však není vše, protože podobně inteligentně zpracovaná jsou i hlášení o chybách, která jsou generovaná při spouštění jednotkových testů. Namísto obvyklé strohé informace o tom, že se například vypočtená kolekce odlišuje od kolekce očekávané, dokáže knihovna Expectations vypsat i další informace, například tehdy, když vrácená kolekce obsahuje jen další prvky (a zbytek kolekce se shoduje s kolekcí očekávanou), některé prvky chybí či se liší pořadí prvků. I v případě vzniku výjimky se namísto celého obsahu zásobníkových rámců vypíšou pouze relevantní informace.
Pro použití této knihovny je nutné upravit projektový soubor project.clj:
(defproject factorial3 "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.6.0"] [expectations "2.0.9"]] :main ^:skip-aot factorial2.core :target-path "target/%s" :plugins [[lein-expectations "0.0.8"] [lein-cloverage "1.0.2"]] :profiles {:uberjar {:aot :all}})
Leiningen nyní umožňuje spouštět novou sadu testů příkazem:
lein expectations Ran 0 tests containing 0 assertions in 8 msecs 0 failures, 0 errors.
Ve skutečnosti využívá knihovna Expectations řídicí kódy terminálu pro obarvení výstupu, takže výsledek může vypadat takto:
15. Testy napsané s využitím knihovny Expectations
Při použití knihovny Expectations se testy zapisují nepatrně odlišným způsobem, než je tomu v případě clojure.test. Především se zde nepoužívá sdružování s využitím deftest a testing, protože autor knihovny Expectations (Jay Fields) se drží zásady „One assertion per test“. Dále se namísto maker is a are, které většinou vyžadují explicitní zápis porovnání, používá inteligentní makro nazvané expect. Toto makro očekává dva parametry. Prvním parametrem je očekávaná hodnota, druhým parametrem je pak většinou volání nějaké funkce či jiného makra.
Makro expect na základě typu prvního parametru automaticky rozpozná, jakým způsobem se má provádět porovnávání; porovnávat lze totiž jak návratové hodnoty (jakéhokoli typu), tak i například zjistit, zda byla vyhozena očekávaná výjimka, zda má návratová hodnota očekávaný typ atd. atd. My nejdříve použijeme test s porovnáváním návratových hodnot a zjišťováním, zda byla vyhozena očekávaná výjimka. V obou případech je použit stejný formát volání makra expect:
(ns factorial2.core-expect-test (:require [factorial2.core :refer :all]) (:use expectations)) (expect 1 (factorial 0)) (expect 1 (factorial 1)) (expect (* 1 2) (factorial 2)) (expect (* 1 2 3 4 5) (factorial 5)) (expect 720 (factorial 6)) (expect 0 (factorial 0)) (expect 0 (factorial 1)) (expect 0 (factorial 2)) (expect IllegalArgumentException (factorial -1)) (expect IllegalArgumentException (factorial -2)) (expect IllegalArgumentException (factorial -100)) (expect IllegalArgumentException (factorial 1)) (expect IllegalArgumentException (factorial 2)) (expect IllegalArgumentException (factorial 3))
Následuje výstup poskytovaný knihovnou Expectations:
failure in (core_expect_test.clj:14) : factorial2.core-expect-test (expect 0 (factorial 0)) expected: 0 was: 1 failure in (core_expect_test.clj:15) : factorial2.core-expect-test (expect 0 (factorial 1)) expected: 0 was: 1 failure in (core_expect_test.clj:16) : factorial2.core-expect-test (expect 0 (factorial 2)) expected: 0 was: 2 failure in (core_expect_test.clj:22) : factorial2.core-expect-test (expect IllegalArgumentException (factorial 1)) (factorial 1) did not throw IllegalArgumentException failure in (core_expect_test.clj:23) : factorial2.core-expect-test (expect IllegalArgumentException (factorial 2)) (factorial 2) did not throw IllegalArgumentException failure in (core_expect_test.clj:24) : factorial2.core-expect-test (expect IllegalArgumentException (factorial 3)) (factorial 3) did not throw IllegalArgumentException Ran 14 tests containing 14 assertions in 71 msecs 6 failures, 0 errors.
Opět platí, že výstup je ve skutečnosti obarven a vypadá zhruba takto:
Takto generovaný výsledek testů je podle mého názoru čitelnější, neboť jsou zobrazeny jen ty skutečně relevantní informace (nicméně výstup z humate-test-output je ještě lepší).
Pokud je prvním parametrem regulární výraz (regexp), bude se „matchovat“ s řetězcem, který je očekáván jako druhý parametr (či výsledek volané funkce/makra). Připomeňme si, že regulární výraz lze v jazyku Clojure považovat za samostatný datový typ a nikoli za běžný řetězec (což zjednodušuje zápis regulárních výrazů):
(expect #"Expect" "Expectations") (expect #"^[a-zA-Z]+$" "Hello") (expect #"^[a-zA-Z]+$" "123qwe") ; nebude splněno (expect #"^[a-zA-Z0-9]+$" "123qwe") ; bude splněno (expect #"[\s]*" "123qwe") (expect #"^([A-Z][a-z]+)+$" "CamelCaseString") ; bude splněno (expect #"^([A-Z][a-z]+)+$" "CamelCaseStringS") ; nebude splněno (expect #"^([A-Z][a-z]+)+$" "camel_case_string") ; nebude splněno
Velmi elegantní je práce s kolekcemi, neboť se automaticky rozpoznávají některé typické odlišnosti dvou kolekcí (očekávané hodnoty a hodnoty vypočtené) – přidání prvku, ubrání prvku, prohození prvků atd. Podívejme se na příklady:
; zjištění existence prvku v kolekci (expect 3 (in [1 2 3])) ; porovnání dvou různých kolekcí (expect [1 2] [3 4]) (expect [1 2] [3 4 5 6]) ; různé typy, stejný obsah - test projde (expect [1 2] '(1 2)) ; expect rozpozná zpřeházené prvky (expect [1 2] [2 1]) (expect [1 2 3] [3 2 1]) ; expect rozpozná přidání prvku (expect [1 2] [1 2 3]) (expect [1 2] [1 2 3 4 5]) ; expect rozpozná i ubrání prvku (expect [1 2 3] [1 2]) (expect [1 2 3 4 5] [1 2]) ; dtto pro mapy - opět se eliminuje výpis zbytečných informací (expect #{:name "Bender" :id 42} #{:name "Bender" :id 42}) (expect #{:name "Bender" :id 42} #{:name "Joe" :id 42}) (expect #{:name "Bender" :id 42} #{:name "Joe" :id 1000}) (expect #{:name "Bender" :id 42} #{:name "Bender" :id 42 :foo :bar}) (expect #{:name "Bender" :id 42} #{:name "Bender"}) (expect #{:name "Bender" :id 42} #{:name "Bender" :not-id 42})
Výsledek běhu testů:
failure in (core_test.clj:67) : expectations-demo.core-test (expect [1 2] [3 4]) expected: [1 2] was: [3 4] in expected, not actual: [1 2] in actual, not expected: [3 4] failure in (core_test.clj:69) : expectations-demo.core-test (expect [1 2] [3 4 5 6]) expected: [1 2] was: [3 4 5 6] in expected, not actual: [1 2] in actual, not expected: [3 4 5 6] actual is larger than expected failure in (core_test.clj:75) : expectations-demo.core-test (expect [1 2] [2 1]) expected: [1 2] was: [2 1] in expected, not actual: [1 2] in actual, not expected: [2 1] lists appear to contain the same items with different ordering failure in (core_test.clj:76) : expectations-demo.core-test (expect [1 2 3] [3 2 1]) expected: [1 2 3] was: [3 2 1] in expected, not actual: [1 nil 3] in actual, not expected: [3 nil 1] lists appear to contain the same items with different ordering failure in (core_test.clj:79) : expectations-demo.core-test (expect [1 2] [1 2 3]) expected: [1 2] was: [1 2 3] in expected, not actual: null in actual, not expected: [nil nil 3] actual is larger than expected failure in (core_test.clj:80) : expectations-demo.core-test (expect [1 2] [1 2 3 4 5]) expected: [1 2] was: [1 2 3 4 5] in expected, not actual: null in actual, not expected: [nil nil 3 4 5] actual is larger than expected failure in (core_test.clj:83) : expectations-demo.core-test (expect [1 2 3] [1 2]) expected: [1 2 3] was: [1 2] in expected, not actual: [nil nil 3] in actual, not expected: null expected is larger than actual failure in (core_test.clj:84) : expectations-demo.core-test (expect [1 2 3 4 5] [1 2]) expected: [1 2 3 4 5] was: [1 2] in expected, not actual: [nil nil 3 4 5] in actual, not expected: null expected is larger than actual failure in (core_test.clj:89) : expectations-demo.core-test (expect #{:name "Bender" :id 42} #{:name "Joe" :id 42}) expected: #{:name "Bender" :id 42} was: #{:name "Joe" :id 42} in expected, not actual: #{"Bender"} in actual, not expected: #{"Joe"} failure in (core_test.clj:91) : expectations-demo.core-test (expect #{:name "Bender" :id 42} #{:name 1000 "Joe" :id}) expected: #{:name "Bender" :id 42} was: #{:name 1000 "Joe" :id} in expected, not actual: #{"Bender" 42} in actual, not expected: #{1000 "Joe"} failure in (core_test.clj:93) : expectations-demo.core-test (expect #{:name "Bender" :id 42} #{:bar :name "Bender" :foo :id 42}) expected: #{:name "Bender" :id 42} was: #{:bar :name "Bender" :foo :id 42} in expected, not actual: null in actual, not expected: #{:bar :foo} failure in (core_test.clj:95) : expectations-demo.core-test (expect #{:name "Bender" :id 42} #{:name "Bender"}) expected: #{:name "Bender" :id 42} was: #{:name "Bender"} in expected, not actual: #{:id 42} in actual, not expected: null failure in (core_test.clj:97) : expectations-demo.core-test (expect #{:name "Bender" :id 42} #{:name "Bender" :not-id 42}) expected: #{:name "Bender" :id 42} was: #{:name "Bender" :not-id 42} in expected, not actual: #{:id} in actual, not expected: #{:not-id}
16. Podpora pro BDD (behaviour-driven development) knihovnou cucumber-jvm-clojure
Jen ve stručnosti se dnes zmíním o posledním nástroji nazvaném cucumber-jvm-clojure. Tento nástroj slouží k podpoře BDD neboli behaviour-driven development. Z pohledu vývojáře se jedná o možnost zápisu testovacích scénářů formalizovaným jazykem blízkým angličtině. Této problematice se budu věnovat v samostatném článku (věnovaném ale spíše Pythonu a modulu behave), takže si jen ve stručnosti ukažme, jak by bylo možné zapsat BDD test pro funkci počítající faktoriál (ve skutečnosti se tyto testy píšou na vyšší úrovni, nejedná se totiž o jednotkové testy):
(ns cucumber-demo.core (:gen-class)) ; funkce faktorial obsahuje i test na zaporne hodnoty (defn factorial [n] (if (neg? n) (throw (IllegalArgumentException. "negative numbers are not supported!")) (apply * (range 1M (inc n))))) ; otestujeme funkci faktorial (defn -main [& args] (doseq [i (range 0 10)] (println i "! = " (factorial i))))
Testovací scénáře skutečně připomínají angličtinu, ovšem slova Feature, Scenario, Given, When, Then a Examples mají speciální význam. Jednodušší forma testu může vypadat takto:
Feature: Factorial computation Scenario: Compute factorial for natural numbers Given The function factorial is callable When I try to compute 2! Then I should get result 2 When I try to compute 3! Then I should get result 6 When I try to compute 10! Then I should get result 3628800
V případě potřeby je možné ten samý test spustit vícekrát s různými parametry (n) a s očekáváním různých výsledků (result):
Feature: Factorial computation #2 Scenario Outline: Compute more factorials for natural numbers Given The function factorial is callable When I try to compute <n>! Then I should get result <result> Examples: | n | result | | 1 | 1 | | 2 | 2 | | 3 | 6 |
V další kapitole si ukážeme, jak připravit projekt akceptující výše uvedené testy.
17. Jednoduchý projekt testovaný s využitím BDD
Projektový soubor je nutné upravit následujícím způsobem. Povšimněte si cesty k testovacím scénářům a současně i ke zdrojovým kódům testů:
(defproject cucumber-demo "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.8.0"]] :plugins [[com.siili/lein-cucumber "1.0.7"]] :cucumber-feature-paths ["test/features/"] :main ^:skip-aot cucumber-demo.core :target-path "target/%s" :profiles {:uberjar {:aot :all} :dev {:dependencies [[com.siili/lein-cucumber "1.0.7"]]}})
Po spuštění příkazu:
lein deps
se nainstaluje nový modul pro Leiningen:
Leiningen is a tool for working with Clojure projects. Several tasks are available: change Rewrite project.clj by applying a function. check Check syntax and warn on reflection. classpath Print the classpath of the current project. clean Remove all files from project's target-path. compile Compile Clojure source into .class files. cucumber Runs Cucumber features in test/features with glue in test/features/step_definitions deploy Build and deploy jar to remote repository. deps Download all dependencies. do Higher-order task to perform other tasks in succession. help Display a list of tasks or help for a given task. install Install the current project to the local repository. jar Package up all the project's files into a jar file. javac Compile Java source files. new Generate project scaffolding based on a template. plugin DEPRECATED. Please use the :user profile instead. pom Write a pom.xml file to disk for Maven interoperability. release Perform :release-tasks. repl Start a repl session either with the current project or standalone. retest Run only the test namespaces which failed last time around. run Run a -main function with optional command-line arguments. search Search remote maven repositories for matching jars. show-profiles List all available profiles or display one if given an argument. test Run the project's tests. trampoline Run a task without nesting the project's JVM inside Leiningen's. uberjar Package up the project files and dependencies into a jar file. update-in Perform arbitrary transformations on your project map. upgrade Upgrade Leiningen to specified version or latest stable. vcs Interact with the version control system. version Print version for Leiningen and the current JVM. with-profile Apply the given task with the profile(s) specified.
Testovací scénáře se uloží do adresáře test/features, což je ostatně patrné i při pohledu na demonstrační příklad:
. ├── doc │ └── intro.md ├── LICENSE ├── project.clj ├── README.md ├── resources ├── src │ └── cucumber_demo │ └── core.clj └── test ├── cucumber_demo │ └── core_test.clj └── features ├── factorial.feature └── step_definitions └── factorial_steps.clj
Nyní musíme napsat implementaci testů, tj. funkce, které se mají provést pro všechny řádky začínající v testovacím scénáři na Given, When a Then. Tyto funkce jsou definovány přes makra (Given), (When) a (Then), za nimiž následuje regulární výraz zpracovávající text scénáře. Taktéž je nutné vyřešit způsob držení informace o kontextu, protože se mezi jednotlivými kroky musí udržovat informace o stavu testu (což jde proti filozofii jazyka Clojure, v němž se snažíme stavovým hodnotám vyhnout):
(use '[clojure.test]) (use '[cucumber-demo.core]) (def context (atom {:input nil :result nil})) (Given #"^The function factorial is callable$" [] (assert (clojure.test/function? 'cucumber-demo.core/factorial))) (When #"^I try to compute (\d+)!$" [input] (let [n (bigdec input)] (swap! context assoc :input n) (swap! context assoc :result (factorial n)))) (Then #"^I should get result (\d+)$" [result_str] (let [expected (bigdec result_str) actual (:result @context)] (assert (= expected actual))))
Podrobnější informace o BDD, jazyku Gherkin i o jeho implementaci v Pythonu a Clojure si řekneme příště.
18. Repositář s demonstračními příklady
Všechny demonstrační příklady a projekty určené pro Clojure verze 1.9.0 byly uloženy do repositáře https://github.com/tisnik/clojure-examples:
Projekt | Popis | Odkaz |
---|---|---|
1 | factorial | https://github.com/tisnik/clojure-examples/tree/master/factorial |
2 | factorial2 | https://github.com/tisnik/clojure-examples/tree/master/factorial2 |
3 | factorial3 | https://github.com/tisnik/clojure-examples/tree/master/factorial3 |
4 | factorial4 | https://github.com/tisnik/clojure-examples/tree/master/factorial4 |
5 | expectations-demo | https://github.com/tisnik/clojure-examples/tree/master/expectations-demo |
6 | cloverage | https://github.com/tisnik/clojure-examples/tree/master/cloverage |
7 | humane-output | https://github.com/tisnik/clojure-examples/tree/master/humane-output |
8 | iota-test | https://github.com/tisnik/clojure-examples/tree/master/iota-test |
9 | cucumber-demo | https://github.com/tisnik/clojure-examples/tree/master/cucumber-demo |
19. Odkazy na předchozí části tohoto seriálu
- Clojure 1: Úvod
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/ - Clojure 2: Symboly, kolekce atd.
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/ - Clojure 3: Funkcionální programování
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-3-cast-funkcionalni-programovani/ - Clojure 4: Kolekce, sekvence a lazy sekvence
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-4-cast-kolekce-sekvence-a-lazy-sekvence/ - Clojure 5: Sekvence, lazy sekvence a paralelní programy
http://www.root.cz/clanky/clojure-a-bezpecne-aplikace-pro-jvm-sekvence-lazy-sekvence-a-paralelni-programy/ - Clojure 6: Podpora pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/ - Clojure 7: Další funkce pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/ - Clojure 8: Identity, stavy, neměnné hodnoty a reference
http://www.root.cz/clanky/programovaci-jazyk-clojure-8-identity-stavy-nemenne-hodnoty-a-referencni-typy/ - Clojure 9: Validátory, pozorovatelé a kooperace s Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-9-validatory-pozorovatele-a-kooperace-mezi-clojure-a-javou/ - Clojure 10: Kooperace mezi Clojure a Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/ - Clojure 11: Generátorová notace seznamu/list comprehension
http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/ - Clojure 12: Překlad programů z Clojure do bajtkódu JVM I:
http://www.root.cz/clanky/programovaci-jazyk-clojure-12-preklad-programu-z-clojure-do-bajtkodu-jvm/ - Clojure 13: Překlad programů z Clojure do bajtkódu JVM II:
http://www.root.cz/clanky/programovaci-jazyk-clojure-13-preklad-programu-z-clojure-do-bajtkodu-jvm-pokracovani/ - Clojure 14: Základy práce se systémem maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/ - Clojure 15: Tvorba uživatelských maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/ - Clojure 16: Složitější uživatelská makra
http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/ - Clojure 17: Využití standardních maker v praxi
http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/ - Clojure 18: Základní techniky optimalizace aplikací
http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/ - Clojure 19: Vývojová prostředí pro Clojure
http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/ - Clojure 20: Vývojová prostředí pro Clojure (Vimu s REPL)
http://www.root.cz/clanky/programovaci-jazyk-clojure-20-vyvojova-prostredi-pro-clojure-integrace-vimu-s-repl/ - Clojure 21: ClojureScript aneb překlad Clojure do JS
http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/ - Leiningen: nástroj pro správu projektů napsaných v Clojure
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (2)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-2/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (3)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-3/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-4/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (5)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-5/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (6)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-6/ - Programovací jazyk Clojure a databáze (1.část)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-databaze-1-cast/ - Pluginy pro Leiningen
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-pluginy-pro-leiningen/ - Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi/ - Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi-2/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-2/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (2)
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-2/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (3)
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-3/ - Programovací jazyk Clojure a práce s Gitem
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (dokončení)
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-dokonceni/ - Programovací jazyk Clojure a práce s Gitem (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem-2/ - Programovací jazyk Clojure – triky při práci s řetězci
http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-retezci/ - Programovací jazyk Clojure – triky při práci s kolekcemi
http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-kolekcemi/ - Programovací jazyk Clojure – práce s mapami a množinami
http://www.root.cz/clanky/programovaci-jazyk-clojure-prace-s-mapami-a-mnozinami/ - Programovací jazyk Clojure – základy zpracování XML
http://www.root.cz/clanky/programovaci-jazyk-clojure-zaklady-zpracovani-xml/ - Programovací jazyk Clojure – testování s využitím knihovny Expectations
http://www.root.cz/clanky/programovaci-jazyk-clojure-testovani-s-vyuzitim-knihovny-expectations/ - Programovací jazyk Clojure – některé užitečné triky použitelné (nejenom) v testech
http://www.root.cz/clanky/programovaci-jazyk-clojure-nektere-uzitecne-triky-pouzitelne-nejenom-v-testech/ - Enlive – výkonný šablonovací systém pro jazyk Clojure
http://www.root.cz/clanky/enlive-vykonny-sablonovaci-system-pro-jazyk-clojure/ - Nástroj Leiningen a programovací jazyk Clojure: tvorba vlastních knihoven pro veřejný repositář Clojars
http://www.root.cz/clanky/nastroj-leiningen-a-programovaci-jazyk-clojure-tvorba-vlastnich-knihoven-pro-verejny-repositar-clojars/ - Novinky v Clojure verze 1.8.0
http://www.root.cz/clanky/novinky-v-clojure-verze-1–8–0/ - Asynchronní programování v Clojure s využitím knihovny core.async
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async/ - Asynchronní programování v Clojure s využitím knihovny core.async (pokračování)
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async-pokracovani/ - Asynchronní programování v Clojure s využitím knihovny core.async (dokončení)
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async-dokonceni/ - Vytváříme IRC bota v programovacím jazyce Clojure
http://www.root.cz/clanky/vytvarime-irc-bota-v-programovacim-jazyce-clojure/ - Gorilla REPL: interaktivní prostředí pro programovací jazyk Clojure
https://www.root.cz/clanky/gorilla-repl-interaktivni-prostredi-pro-programovaci-jazyk-clojure/ - Multimetody v Clojure aneb polymorfismus bez použití OOP
https://www.root.cz/clanky/multimetody-v-clojure-aneb-polymorfismus-bez-pouziti-oop/ - Práce s externími Java archivy v programovacím jazyku Clojure
https://www.root.cz/clanky/prace-s-externimi-java-archivy-v-programovacim-jazyku-clojure/ - Pixie: lehký skriptovací jazyk s „kouzelnými“ schopnostmi
https://www.root.cz/clanky/pixie-lehky-skriptovaci-jazyk-s-kouzelnymi-schopnostmi/ - Programovací jazyk Pixie: funkce ze základní knihovny a použití FFI
https://www.root.cz/clanky/programovaci-jazyk-pixie-funkce-ze-zakladni-knihovny-a-pouziti-ffi/ - Novinky v Clojure verze 1.9.0
https://www.root.cz/clanky/novinky-v-clojure-verze-1–9–0/ - Validace dat s využitím knihovny spec v Clojure 1.9.0
https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0/
20. Odkazy na Internetu
- Humane test output for clojure.test
https://github.com/pjstadig/humane-test-output - iota
https://github.com/juxt/iota - 5 Differences between clojure.spec and Schema
https://lispcast.com/clojure.spec-vs-schema/ - Schema: Clojure(Script) library for declarative data description and validation
https://github.com/plumatic/schema - Zip archiv s Clojure 1.9.0
http://repo1.maven.org/maven2/org/clojure/clojure/1.9.0/clojure-1.9.0.zip - Clojure 1.9 is now available
https://clojure.org/news/2017/12/08/clojure19 - Deps and CLI Guide
https://clojure.org/guides/deps_and_cli - Changes to Clojure in Version 1.9
https://github.com/clojure/clojure/blob/master/changes.md - clojure.spec – Rationale and Overview
https://clojure.org/about/spec - Zip archiv s Clojure 1.8.0
http://repo1.maven.org/maven2/org/clojure/clojure/1.8.0/clojure-1.8.0.zip - Clojure 1.8 is now available
http://clojure.org/news/2016/01/19/clojure18 - Socket Server REPL
http://dev.clojure.org/display/design/Socket+Server+REPL - CLJ-1671: Clojure socket server
http://dev.clojure.org/jira/browse/CLJ-1671 - CLJ-1449: Add clojure.string functions for portability to ClojureScript
http://dev.clojure.org/jira/browse/CLJ-1449 - Launching a Socket Server
http://clojure.org/reference/repl_and_main#_launching_a_socket_server - API for clojure.string
http://clojure.github.io/clojure/branch-master/clojure.string-api.html - Clojars:
https://clojars.org/ - Seznam knihoven na Clojars:
https://clojars.org/projects - Clojure Cookbook: Templating HTML with Enlive
https://github.com/clojure-cookbook/clojure-cookbook/blob/master/07_webapps/7–11_enlive.asciidoc - An Introduction to Enlive
https://github.com/swannodette/enlive-tutorial/ - Enlive na GitHubu
https://github.com/cgrand/enlive - Expectations: příklady atd.
http://jayfields.com/expectations/ - Expectations na GitHubu
https://github.com/jaycfields/expectations - Lein-expectations na GitHubu
https://github.com/gar3thjon3s/lein-expectations - Testing Clojure With Expectations
https://semaphoreci.com/blog/2014/09/23/testing-clojure-with-expectations.html - Clojure testing TDD/BDD libraries: clojure.test vs Midje vs Expectations vs Speclj
https://www.reddit.com/r/Clojure/comments/1viilt/clojure_testing_tddbdd_libraries_clojuretest_vs/ - Testing: One assertion per test
http://blog.jayfields.com/2007/06/testing-one-assertion-per-test.html - Rewriting Your Test Suite in Clojure in 24 hours
http://blog.circleci.com/rewriting-your-test-suite-in-clojure-in-24-hours/ - Clojure doc: zipper
http://clojuredocs.org/clojure.zip/zipper - Clojure doc: parse
http://clojuredocs.org/clojure.xml/parse - Clojure doc: xml-zip
http://clojuredocs.org/clojure.zip/xml-zip - Clojure doc: xml-seq
http://clojuredocs.org/clojure.core/xml-seq - Parsing XML in Clojure
https://github.com/clojuredocs/guides - Clojure Zipper Over Nested Vector
https://vitalyper.wordpress.com/2010/11/23/clojure-zipper-over-nested-vector/ - Understanding Clojure's PersistentVector implementation
http://blog.higher-order.net/2009/02/01/understanding-clojures-persistentvector-implementation - Understanding Clojure's PersistentHashMap (deftwice…)
http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice.html - Assoc and Clojure's PersistentHashMap: part ii
http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap-part-ii.html - Ideal Hashtrees (paper)
http://lampwww.epfl.ch/papers/idealhashtrees.pdf - Clojure home page
http://clojure.org/ - Clojure (downloads)
http://clojure.org/downloads - Clojure Sequences
http://clojure.org/sequences - Clojure Data Structures
http://clojure.org/data_structures - The Structure and Interpretation of Computer Programs: 2.2.1 Representing Sequences
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-15.html#%_sec2.2.1 - The Structure and Interpretation of Computer Programs: 3.3.1 Mutable List Structure
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-22.html#%_sec3.3.1 - Clojure – Functional Programming for the JVM
http://java.ociweb.com/mark/clojure/article.html - Clojure quick reference
http://faustus.webatu.com/clj-quick-ref.html - 4Clojure
http://www.4clojure.com/ - ClojureDoc (rozcestník s dokumentací jazyka Clojure)
http://clojuredocs.org/ - Clojure (na Wikipedia EN)
http://en.wikipedia.org/wiki/Clojure - Clojure (na Wikipedia CS)
http://cs.wikipedia.org/wiki/Clojure - SICP (The Structure and Interpretation of Computer Programs)
http://mitpress.mit.edu/sicp/ - Pure function
http://en.wikipedia.org/wiki/Pure_function - Funkcionální programování
http://cs.wikipedia.org/wiki/Funkcionální_programování - Čistě funkcionální (datové struktury, jazyky, programování)
http://cs.wikipedia.org/wiki/Čistě_funkcionální - Clojure Macro Tutorial (Part I, Getting the Compiler to Write Your Code For You)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-i-getting.html - Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html - Clojure Macro Tutorial (Part III: Syntax Quote)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html - Tech behind Tech: Clojure Macros Simplified
http://techbehindtech.com/2010/09/28/clojure-macros-simplified/ - Fatvat – Exploring functional programming: Clojure Macros
http://www.fatvat.co.uk/2009/02/clojure-macros.html - Eulerovo číslo
http://cs.wikipedia.org/wiki/Eulerovo_číslo - List comprehension
http://en.wikipedia.org/wiki/List_comprehension - List Comprehensions in Clojure
http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html - Clojure Programming Concepts: List Comprehension
http://en.wikibooks.org/wiki/Clojure_Programming/Concepts#List_Comprehension - Clojure core API: for macro
http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/for - cirrus machina – The Clojure for macro
http://www.cirrusmachina.com/blog/comment/the-clojure-for-macro/ - Riastradh's Lisp Style Rules
http://mumble.net/~campbell/scheme/style.txt - Dynamic Languages Strike Back
http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html - Scripting: Higher Level Programming for the 21st Century
http://www.tcl.tk/doc/scripting.html - Java Virtual Machine Support for Non-Java Languages
http://docs.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html - Třída java.lang.String
http://docs.oracle.com/javase/7/docs/api/java/lang/String.html - Třída java.lang.StringBuffer
http://docs.oracle.com/javase/7/docs/api/java/lang/StringBuffer.html - Třída java.lang.StringBuilder
http://docs.oracle.com/javase/7/docs/api/java/lang/StringBuilder.html - StringBuffer versus String
http://www.javaworld.com/article/2076072/build-ci-sdlc/stringbuffer-versus-string.html - Threading macro (dokumentace k jazyku Clojure)
https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/-> - Understanding the Clojure → macro
http://blog.fogus.me/2009/09/04/understanding-the-clojure-macro/ - clojure.inspector
http://clojure.github.io/clojure/clojure.inspector-api.html - The Clojure Toolbox
http://www.clojure-toolbox.com/ - Unit Testing in Clojure
http://nakkaya.com/2009/11/18/unit-testing-in-clojure/ - Testing in Clojure (Part-1: Unit testing)
http://blog.knoldus.com/2014/03/22/testing-in-clojure-part-1-unit-testing/ - API for clojure.test – Clojure v1.6 (stable)
https://clojure.github.io/clojure/clojure.test-api.html - Leiningen: úvodní stránka
http://leiningen.org/ - Leiningen: Git repository
https://github.com/technomancy/leiningen - leiningen-win-installer
http://leiningen-win-installer.djpowell.net/ - Clojure.org: Vars and the Global Environment
http://clojure.org/Vars - Clojure.org: Refs and Transactions
http://clojure.org/Refs - Clojure.org: Atoms
http://clojure.org/Atoms - Clojure.org: Agents as Asynchronous Actions
http://clojure.org/agents - Transient Data Structureshttp://clojure.org/transients