Hlavní navigace

Knihovny a moduly usnadňující testování aplikací naprogramovaných v jazyce Clojure

Pavel Tišnovský

Dnes se seznámíme s knihovnami a moduly pro Leiningen určenými pro testování aplikací v jazyce Clojure. V této oblasti vzniklo mnoho zajímavých projektů, například modul humane-test-output, knihovna iota či adaptace jazyka Gherkin pro BDD.

Doba čtení: 49 minut

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

20. Odkazy na Internetu

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.

Poznámka: v dalším textu se předpokládá, že máte nainstalován správce projektu Leiningen. Pokud tomu tak není, bude nutné si Leiningen nainstalovat, což není nic těžkého. Navíc se jedná o velmi užitečný projekt s mnoha přídavnými moduly, které využijete nejenom při testování.

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í.

Poznámka: mohlo by se zdát, že namísto is je možné použít standardní makro assert. Ve skutečnosti by však správně nefungovalo vyhodnocení testů, i když je činnost makra assert zdánlivě velmi podobná:
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
Poznámka: modul humane-test-output je kompatibilní s Clojure 1.8.0 a samozřejmě i s vyššími verzemi. Pozor si dejte především na to, že pouhé spuštění příkazu lein repl mimo vlastní projekt může u starších verzí Leiningenu ve skutečnosti spustit interpret starší varianty Clojure, například i dnes již notně zastaralé varianty 1.6.0. Při spuštění lein repl v adresáři projektu se ovšem korektně použije interpret nastavený v souboru project.clj.

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

MIF18 tip v článku Steiner

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

Poznámka: pro spuštění projektů je vyžadován nainstalovaný správce projektů Leiningen.

19. Odkazy na předchozí části tohoto seriálu

  1. Clojure 1: Úvod
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/
  2. Clojure 2: Symboly, kolekce atd.
    http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/
  3. 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/
  4. 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/
  5. 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/
  6. Clojure 6: Podpora pro paralelní programování
    http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/
  7. Clojure 7: Další funkce pro paralelní programování
    http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/
  8. 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/
  9. 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/
  10. Clojure 10: Kooperace mezi Clojure a Javou
    http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/
  11. Clojure 11: Generátorová notace seznamu/list comprehension
    http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/
  12. 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/
  13. 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/
  14. Clojure 14: Základy práce se systémem maker
    http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/
  15. Clojure 15: Tvorba uživatelských maker
    http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/
  16. Clojure 16: Složitější uživatelská makra
    http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/
  17. Clojure 17: Využití standardních maker v praxi
    http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/
  18. Clojure 18: Základní techniky optimalizace aplikací
    http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/
  19. Clojure 19: Vývojová prostředí pro Clojure
    http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/
  20. 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/
  21. Clojure 21: ClojureScript aneb překlad Clojure do JS
    http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/
  22. Leiningen: nástroj pro správu projektů napsaných v Clojure
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/
  23. 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/
  24. 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/
  25. 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/
  26. 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/
  27. 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/
  28. Programovací jazyk Clojure a databáze (1.část)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-databaze-1-cast/
  29. Pluginy pro Leiningen
    http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-pluginy-pro-leiningen/
  30. 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/
  31. 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/
  32. 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/
  33. 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/
  34. Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure
    http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure/
  35. 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/
  36. 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/
  37. Programovací jazyk Clojure a práce s Gitem
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem/
  38. 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/
  39. Programovací jazyk Clojure a práce s Gitem (2)
    http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem-2/
  40. Programovací jazyk Clojure – triky při práci s řetězci
    http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-retezci/
  41. Programovací jazyk Clojure – triky při práci s kolekcemi
    http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-kolekcemi/
  42. Programovací jazyk Clojure – práce s mapami a množinami
    http://www.root.cz/clanky/programovaci-jazyk-clojure-prace-s-mapami-a-mnozinami/
  43. Programovací jazyk Clojure – základy zpracování XML
    http://www.root.cz/clanky/programovaci-jazyk-clojure-zaklady-zpracovani-xml/
  44. Programovací jazyk Clojure – testování s využitím knihovny Expectations
    http://www.root.cz/clanky/programovaci-jazyk-clojure-testovani-s-vyuzitim-knihovny-expectations/
  45. 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/
  46. Enlive – výkonný šablonovací systém pro jazyk Clojure
    http://www.root.cz/clanky/enlive-vykonny-sablonovaci-system-pro-jazyk-clojure/
  47. 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/
  48. Novinky v Clojure verze 1.8.0
    http://www.root.cz/clanky/novinky-v-clojure-verze-1–8–0/
  49. 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/
  50. 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/
  51. 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/
  52. Vytváříme IRC bota v programovacím jazyce Clojure
    http://www.root.cz/clanky/vytvarime-irc-bota-v-programovacim-jazyce-clojure/
  53. Gorilla REPL: interaktivní prostředí pro programovací jazyk Clojure
    https://www.root.cz/clanky/gorilla-repl-interaktivni-prostredi-pro-programovaci-jazyk-clojure/
  54. Multimetody v Clojure aneb polymorfismus bez použití OOP
    https://www.root.cz/clanky/multimetody-v-clojure-aneb-polymorfismus-bez-pouziti-oop/
  55. 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/
  56. Pixie: lehký skriptovací jazyk s „kouzelnými“ schopnostmi
    https://www.root.cz/clanky/pixie-lehky-skriptovaci-jazyk-s-kouzelnymi-schopnostmi/
  57. Programovací jazyk Pixie: funkce ze základní knihovny a použití FFI
    https://www.root.cz/clanky/pro­gramovaci-jazyk-pixie-funkce-ze-zakladni-knihovny-a-pouziti-ffi/
  58. Novinky v Clojure verze 1.9.0
    https://www.root.cz/clanky/novinky-v-clojure-verze-1–9–0/
  59. 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

  1. Humane test output for clojure.test
    https://github.com/pjstadig/humane-test-output
  2. iota
    https://github.com/juxt/iota
  3. 5 Differences between clojure.spec and Schema
    https://lispcast.com/clojure.spec-vs-schema/
  4. Schema: Clojure(Script) library for declarative data description and validation
    https://github.com/plumatic/schema
  5. Zip archiv s Clojure 1.9.0
    http://repo1.maven.org/ma­ven2/org/clojure/clojure/1­.9.0/clojure-1.9.0.zip
  6. Clojure 1.9 is now available
    https://clojure.org/news/2017/12/08/clo­jure19
  7. Deps and CLI Guide
    https://clojure.org/guides/dep­s_and_cli
  8. Changes to Clojure in Version 1.9
    https://github.com/clojure/clo­jure/blob/master/changes.md
  9. clojure.spec – Rationale and Overview
    https://clojure.org/about/spec
  10. Zip archiv s Clojure 1.8.0
    http://repo1.maven.org/ma­ven2/org/clojure/clojure/1­.8.0/clojure-1.8.0.zip
  11. Clojure 1.8 is now available
    http://clojure.org/news/2016/01/19/clo­jure18
  12. Socket Server REPL
    http://dev.clojure.org/dis­play/design/Socket+Server+REPL
  13. CLJ-1671: Clojure socket server
    http://dev.clojure.org/jira/browse/CLJ-1671
  14. CLJ-1449: Add clojure.string functions for portability to ClojureScript
    http://dev.clojure.org/jira/browse/CLJ-1449
  15. Launching a Socket Server
    http://clojure.org/referen­ce/repl_and_main#_launchin­g_a_socket_server
  16. API for clojure.string
    http://clojure.github.io/clo­jure/branch-master/clojure.string-api.html
  17. Clojars:
    https://clojars.org/
  18. Seznam knihoven na Clojars:
    https://clojars.org/projects
  19. Clojure Cookbook: Templating HTML with Enlive
    https://github.com/clojure-cookbook/clojure-cookbook/blob/master/07_webapps/7–11_enlive.asciidoc
  20. An Introduction to Enlive
    https://github.com/swannodette/enlive-tutorial/
  21. Enlive na GitHubu
    https://github.com/cgrand/enlive
  22. Expectations: příklady atd.
    http://jayfields.com/expectations/
  23. Expectations na GitHubu
    https://github.com/jaycfi­elds/expectations
  24. Lein-expectations na GitHubu
    https://github.com/gar3thjon3s/lein-expectations
  25. Testing Clojure With Expectations
    https://semaphoreci.com/blog/2014/09/23/tes­ting-clojure-with-expectations.html
  26. Clojure testing TDD/BDD libraries: clojure.test vs Midje vs Expectations vs Speclj
    https://www.reddit.com/r/Clo­jure/comments/1viilt/cloju­re_testing_tddbdd_librari­es_clojuretest_vs/
  27. Testing: One assertion per test
    http://blog.jayfields.com/2007/06/tes­ting-one-assertion-per-test.html
  28. Rewriting Your Test Suite in Clojure in 24 hours
    http://blog.circleci.com/rewriting-your-test-suite-in-clojure-in-24-hours/
  29. Clojure doc: zipper
    http://clojuredocs.org/clo­jure.zip/zipper
  30. Clojure doc: parse
    http://clojuredocs.org/clo­jure.xml/parse
  31. Clojure doc: xml-zip
    http://clojuredocs.org/clojure.zip/xml-zip
  32. Clojure doc: xml-seq
    http://clojuredocs.org/clo­jure.core/xml-seq
  33. Parsing XML in Clojure
    https://github.com/clojuredocs/guides
  34. Clojure Zipper Over Nested Vector
    https://vitalyper.wordpres­s.com/2010/11/23/clojure-zipper-over-nested-vector/
  35. Understanding Clojure's PersistentVector implementation
    http://blog.higher-order.net/2009/02/01/understanding-clojures-persistentvector-implementation
  36. Understanding Clojure's PersistentHashMap (deftwice…)
    http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice.html
  37. Assoc and Clojure's PersistentHashMap: part ii
    http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap-part-ii.html
  38. Ideal Hashtrees (paper)
    http://lampwww.epfl.ch/pa­pers/idealhashtrees.pdf
  39. Clojure home page
    http://clojure.org/
  40. Clojure (downloads)
    http://clojure.org/downloads
  41. Clojure Sequences
    http://clojure.org/sequences
  42. Clojure Data Structures
    http://clojure.org/data_structures
  43. 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
  44. 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
  45. Clojure – Functional Programming for the JVM
    http://java.ociweb.com/mar­k/clojure/article.html
  46. Clojure quick reference
    http://faustus.webatu.com/clj-quick-ref.html
  47. 4Clojure
    http://www.4clojure.com/
  48. ClojureDoc (rozcestník s dokumentací jazyka Clojure)
    http://clojuredocs.org/
  49. Clojure (na Wikipedia EN)
    http://en.wikipedia.org/wiki/Clojure
  50. Clojure (na Wikipedia CS)
    http://cs.wikipedia.org/wiki/Clojure
  51. SICP (The Structure and Interpretation of Computer Programs)
    http://mitpress.mit.edu/sicp/
  52. Pure function
    http://en.wikipedia.org/wi­ki/Pure_function
  53. Funkcionální programování
    http://cs.wikipedia.org/wi­ki/Funkcionální_programová­ní
  54. Čistě funkcionální (datové struktury, jazyky, programování)
    http://cs.wikipedia.org/wi­ki/Čistě_funkcionální
  55. 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
  56. Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html
  57. Clojure Macro Tutorial (Part III: Syntax Quote)
    http://www.learningclojure­.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html
  58. Tech behind Tech: Clojure Macros Simplified
    http://techbehindtech.com/2010/09/28/clo­jure-macros-simplified/
  59. Fatvat – Exploring functional programming: Clojure Macros
    http://www.fatvat.co.uk/2009/02/clo­jure-macros.html
  60. Eulerovo číslo
    http://cs.wikipedia.org/wi­ki/Eulerovo_číslo
  61. List comprehension
    http://en.wikipedia.org/wi­ki/List_comprehension
  62. List Comprehensions in Clojure
    http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html
  63. Clojure Programming Concepts: List Comprehension
    http://en.wikibooks.org/wi­ki/Clojure_Programming/Con­cepts#List_Comprehension
  64. Clojure core API: for macro
    http://clojure.github.com/clo­jure/clojure.core-api.html#clojure.core/for
  65. cirrus machina – The Clojure for macro
    http://www.cirrusmachina.com/blog/com­ment/the-clojure-for-macro/
  66. Riastradh's Lisp Style Rules
    http://mumble.net/~campbe­ll/scheme/style.txt
  67. Dynamic Languages Strike Back
    http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html
  68. Scripting: Higher Level Programming for the 21st Century
    http://www.tcl.tk/doc/scripting.html
  69. Java Virtual Machine Support for Non-Java Languages
    http://docs.oracle.com/ja­vase/7/docs/technotes/gui­des/vm/multiple-language-support.html
  70. Třída java.lang.String
    http://docs.oracle.com/ja­vase/7/docs/api/java/lang/Strin­g.html
  71. Třída java.lang.StringBuffer
    http://docs.oracle.com/ja­vase/7/docs/api/java/lang/Strin­gBuffer.html
  72. Třída java.lang.StringBuilder
    http://docs.oracle.com/ja­vase/7/docs/api/java/lang/Strin­gBuilder.html
  73. StringBuffer versus String
    http://www.javaworld.com/ar­ticle/2076072/build-ci-sdlc/stringbuffer-versus-string.html
  74. Threading macro (dokumentace k jazyku Clojure)
    https://clojure.github.io/clo­jure/clojure.core-api.html#clojure.core/->
  75. Understanding the Clojure → macro
    http://blog.fogus.me/2009/09/04/un­derstanding-the-clojure-macro/
  76. clojure.inspector
    http://clojure.github.io/clo­jure/clojure.inspector-api.html
  77. The Clojure Toolbox
    http://www.clojure-toolbox.com/
  78. Unit Testing in Clojure
    http://nakkaya.com/2009/11/18/unit-testing-in-clojure/
  79. Testing in Clojure (Part-1: Unit testing)
    http://blog.knoldus.com/2014/03/22/tes­ting-in-clojure-part-1-unit-testing/
  80. API for clojure.test – Clojure v1.6 (stable)
    https://clojure.github.io/clo­jure/clojure.test-api.html
  81. Leiningen: úvodní stránka
    http://leiningen.org/
  82. Leiningen: Git repository
    https://github.com/techno­mancy/leiningen
  83. leiningen-win-installer
    http://leiningen-win-installer.djpowell.net/
  84. Clojure.org: Vars and the Global Environment
    http://clojure.org/Vars
  85. Clojure.org: Refs and Transactions
    http://clojure.org/Refs
  86. Clojure.org: Atoms
    http://clojure.org/Atoms
  87. Clojure.org: Agents as Asynchronous Actions
    http://clojure.org/agents
  88. Transient Data Structureshttp://clojure.or­g/transients
Našli jste v článku chybu?