Obsah
1. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
2. Jednoduchý testovací scénář pro kontrolu funkčnosti REST API GitHubu
3. Struktura projektu pro spuštění testovacího scénáře
4. Implementace jednotlivých kroků testovacího scénáře
5. Spuštění testovacího scénáře
6. Zjednodušení projektu s testovacím scénářem
7. Rozšíření testovacích scénářů – vyhledání uživatelů GitHubu
8. Spuštění testovacích scénářů ve chvíli, kdy nejsou implementovány všechny požadované kroky
9. Vytvoření projektu s novými závislostmi
10. Implementace nových kroků vyžadovaných testovacími scénáři
11. Spuštění testovacích scénářů kompletního projektu
12. Vylepšení testovacího scénáře – vyhledání informací o větším množství uživatelů
13. Opětovné spuštění testovacích scénářů
14. Malá úprava testů pro uživatele GitHubu bez vyplněné společnosti
15. Testovací scénář použitelný pro libovolné REST API, nejenom pro GitHub
17. Repositář s demonstračními příklady
18. Odkazy na předchozí části tohoto seriálu
1. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
V dnešním článku o programovacím jazyce Clojure dokončíme téma, kterému jsme se začali věnovat minule. Již v předchozím článku jsme si totiž ukázali, jakým způsobem je možné zaintegrovat testovací scénáře popsané v jazyku Gherkin do projektu naprogramovaného v Clojure. Samotné kroky prováděné v testech byly napsány s využitím knihovny Expectations, což je – samozřejmě jen pouze podle mého názoru – téměř ideální kombinace (jak již víme, je přímé použití clojure.test příliš nízkoúrovňové). Dnes si ukážeme poněkud praktičtější použití při testování REST API. Pro jednoduchost nebudeme testovat API vlastního serveru, ale budeme testy spouštět oproti API GitHubu, které je dostatečně zdokumentováno a pro některé operace se dokonce ani nebudeme muset přihlašovat :-)
Obrázek 1: Ukázka scénářů napsaných v jazyce Gherkin.
2. Jednoduchý testovací scénář pro kontrolu funkčnosti REST API GitHubu
Nejprve si připravíme soubor s prozatím jediným testovacím scénářem. V tomto scénáři, který bude skutečně velmi jednoduchý, budeme vycházet z předpokladu (Given), že je dostupný web GitHubu. Pokud tomu tak je, tak se pokusíme přistoupit na výchozí bod REST API GitHubu metodou GET (When). Očekáváme přitom, že služba odpoví běžným způsobem a vrátí stavový kód 200 OK (Then). Celý scénář je uložen v souboru nazvaném github.feature, vypadá následovně:
Feature: GitHub API tests @smoketest Scenario: Check the GitHub API entry point Given GitHub is accessible When I access the API endpoint / Then I should receive response with 200 status code
Poznámka: řádkem obsahujícím @smoketest byl ke scénáři přiřazen tag. Díky použití tagů lze jednoduše vybírat, které testy se mají spustit.
3. Struktura projektu pro spuštění testovacího scénáře
Testovací scénář bude uložen do nového projektu, který nazveme (kvůli mé malé obrazotvornosti) jednoduše „cucumber+expect7“. Základní strukturu projektu opět, jak je již zvykem, vytvoříme příkazem:
$ lein new app cucumber+expect7
Výsledkem by měla být tato adresářová struktura:
├── doc │ └── intro.md ├── LICENSE ├── project.clj ├── README.md ├── resources ├── src │ └── cucumber+expect7 │ └── core.clj └── test └── cucumber+expect7 └── core_test.clj 6 directories, 6 files
Následně do projektu přidáme adresář „features“ a v něm vytvoříme soubor s výše popsaným testovacím scénářem. Dále ještě vytvoříme podadresář „features/step_definitions“ s prozatím prázdným souborem pojmenovaným „github_steps.clj“. Nová adresářová struktura projektu bude nyní vypadat následovně:
├── bdd ├── doc │ └── intro.md ├── features │ ├── github.feature │ └── step_definitions │ └── github_steps.clj ├── LICENSE ├── project.clj ├── README.md ├── resources ├── src │ └── cucumber+expect7 │ └── core.clj └── test └── cucumber+expect7 └── core_test.clj 8 directories, 9 files
Provést musíme ještě jednu důležitou změnu, a to konkrétně úpravu projektového souboru „project.clj“. Do něj přidáme řádky popisující přídavné moduly a knihovny, které použijeme v testech. Kromě toho také musíme přidat klíč s informací o tom, kde se mají hledat testovací scénáře:
(defproject cucumber+expect7 "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"] [expectations "2.0.9"]] :plugins [[com.siili/lein-cucumber "1.0.7"] [lein-expectations "0.0.8"]] :cucumber-feature-paths ["features/"] :main ^:skip-aot cucumber+expect7.core :target-path "target/%s" :profiles {:uberjar {:aot :all} :dev {:dependencies [[com.siili/lein-cucumber "1.0.7"]]}})
4. Implementace jednotlivých kroků testovacího scénáře
Nyní musíme implementovat jednotlivé kroky testovacího scénáře. Tyto kroky budou uloženy ve zdrojovém kódu step_definitions/github_steps.clj. Celý skript je rozdělen do několika částí.
Nejdříve jsou uvedeny importy dalších modulů, které budou v testu použity. Zejména budeme využívat již popsanou knihovnu Expectations a v ní deklarované makro expect:
(use '[cucumber+expect7.core]) (require '[expectations :refer [expect]])
Následně si pro jednoduchost vytvoříme dva symboly, v nichž budou uloženy adresy GitHubu i základ adresy služeb GitHubu dostupných přes REST API (v ideálním případě by se tyto konstanty načetly ze souborů, více viz další text):
(def URL "https://github.com") (def API-URL "https://api.github.com")
Další symbol je navázán na atom obsahující kontext testů, tj. stav, který může být v jednotlivých krocích buď měněn nebo testován jeho obsah. O významu kontextu jsme se zmínili minule (ve skutečnosti není nutné používat zrovna atom, ale je to nejjednodušší řešení):
(def context (atom {:response nil}))
Další část skriptu obsahuje definici funkce určené pro přístup k REST API či k libovolné URL. První řádek není nutné použít v případě, že Clojure používá Javu 8 nebo Javu 9, ovšem v Javě 7 či dokonce v Javě 6 je nutné povolit novější verze TLS. Ostatně i z tohoto důvodu zde nepoužívám žádnou knihovnu typu clj-http, protože se tím nastavení protokolu https dosti komplikuje (popravdě řečeno jsem nepřišel na to, jak tento problém v Javě 7 a Javě 6 elegantně řešit). Samotná funkce request je jednoduchá – otevře připojení na zadanou adresu, přečte data ze vstupního proudu a uloží tato data a současně i stavový kód HTTP do kontextu:
(System/setProperty "https.protocols" "TLSv1,TLSv1.1,TLSv1.2") (defn request [address] (let [url (new java.net.URL address) connection (.openConnection url)] (.connect connection) {:status (.getResponseCode connection) :content (slurp (.getInputStream connection))}))
Následuje implementace všech tří kroků testu – předpokladu, který musí být splněn před spuštěním, kroku, v němž přistupujeme k URL GitHubu a konečně ke kontrole stavového kódu HTTP, který se vrátil. Očekávaný kód získáme parsingem řádku zapsaného v testovacím scénáři:
(Given #"^GitHub is accessible$" [] (let [response (request URL)] (expect (:status response) 200))) (When #"^I access the API endpoint /$" [] (let [response (request (str API-URL "/"))] (swap! context assoc :response response))) (Then #"^I should receive response with (\d+) status code$" [code] (let [expected_code (Integer/parseInt code) actual_code (-> @context :response :status)] (expect expected_code actual_code)))
5. Spuštění testovacího scénáře
Připomeňme si, že o spuštění testovacího scénáře se postará příkaz:
$ lein cucumber
Pokud tento příkaz skutečně spustíme, měly by se vypsat následující řádky (samozřejmě za předpokladu, že je GitHub dostupný). Výsledek bude vypadat zhruba následovně:
Running cucumber... Looking for features in: [/home/tester/temp/clojure/clojure-examples/cucumber+expect7/features] Looking for glue in: [/home/tester/temp/clojure/clojure-examples/cucumber+expect7/features/step_definitions] ... 1 Scenarios (1 passed) 3 Steps (3 passed) 0m1.627s Ran 2 tests containing 2 assertions in 11 msecs 0 failures, 0 errors.
Z výsledků je patrné, že se scénář (prozatím jediný) skutečně spustil a všechny tři jeho kroky proběhly bez chyby.
Obrázek 2: Skutečná podoba výstupu po spuštění testovacího scénáře z příkazové řádky.
6. Zjednodušení projektu s testovacím scénářem
Ve skutečnosti byl předchozí projekt příliš složitý a obsahoval několik adresářů a souborů, které jsme žádným způsobem nemohli využít. Je tomu tak z toho důvodu, že základní struktura projektu byla vytvořena nástrojem Leiningen, který pouze využívá předpřipravené šablony. Pro naše účely však vůbec nebudeme potřebovat ani zdrojové kódy uložené v adresáři „src“ ani jednotkové testy uložené standardně v adresáři „test“. Původní struktura projektu vypadala následovně:
├── bdd ├── doc │ └── intro.md ├── features │ ├── github.feature │ └── step_definitions │ └── github_steps.clj ├── LICENSE ├── project.clj ├── README.md ├── resources ├── src │ └── cucumber+expect7 │ └── core.clj └── test └── cucumber+expect7 └── core_test.clj 8 directories, 9 files
Pokud odstraníme výše zmíněné adresáře „src“ a „test“ i s jejich obsahem, získáme jednodušší projekt:
├── bdd ├── doc │ └── intro.md ├── features │ ├── github.feature │ └── step_definitions │ └── github_steps.clj ├── LICENSE ├── project.clj ├── README.md └── resources 4 directories, 7 files
Navíc ovšem musíme z projektového souboru „project.clj“ odstranit řádek:
:main ^:skip-aot cucumber+expect7.core
Výsledný soubor, který je tentokrát součástí druhého demonstračního příkladu, vypadá takto:
(defproject cucumber+expect8 "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"] [expectations "2.0.9"]] :plugins [[com.siili/lein-cucumber "1.0.7"] [lein-expectations "0.0.8"]] :cucumber-feature-paths ["features/"] :target-path "target/%s" :profiles {:uberjar {:aot :all} :dev {:dependencies [[com.siili/lein-cucumber "1.0.7"]]}})
Od této chvíle samozřejmě nebude možné použít příkaz pro spuštění aplikace (protože žádná skutečná aplikace neexistuje):
$ lein run No :main namespace specified in project.clj.
Pokus o spuštění jednotkových testů také dopadne poněkud podezřele (i když je pravda, že pokud žádné testy neexistují, je statistika vypsána dobře):
$ lein test lein test user Ran 0 tests containing 0 assertions. 0 failures, 0 errors.
Spuštění testovacího scénáře je naproti tomu samozřejmě možné:
$ lein cucumber Running cucumber... Looking for features in: [/home/tester/temp/clojure/clojure-examples/cucumber+expect8/features] Looking for glue in: [/home/tester/temp/clojure/clojure-examples/cucumber+expect8/features/step_definitions] ... 1 Scenarios (1 passed) 3 Steps (3 passed) 0m1.906s Ran 2 tests containing 2 assertions in 12 msecs 0 failures, 0 errors.
7. Rozšíření testovacích scénářů – vyhledání uživatelů GitHubu
Nyní se zaměřme na nepatrně složitější testovací scénář. Budeme v něm na GitHubu vyhledávat uživatele podle jejich nicku (přezdívky) a zjišťovat jejich skutečné jméno a firmu, pro kterou pracují. Pro tento účel existuje v REST API GitHubu příslušný endpoint, takže implementace testovacích kroků bude poměrně jednoduchá. Nejprve si ukažme první verzi scénáře. Může vypadat takto:
Feature: GitHub API tests @smoketest Scenario: Check the GitHub API entry point Given GitHub is accessible When I access the API endpoint / Then I should receive response with 200 status code Scenario: Check the user search feature Given GitHub is accessible When I search for user with nick torvalds Then I should receive response with 200 status code Then I should receive proper JSON response Then I should find the user with full name Linus Torvalds Then I should find that the user works for company Linux Foundation
Ve skutečnosti se tímto způsobem většinou scénáře nepíšou, a to z toho důvodu, že série za sebou jdoucích vět začínajících slovem Then nevypadá příliš čitelně. Namísto toho je ale možné použít And s prakticky stejným významem (zavolají se naprosto stejné testovací kroky, samozřejmě pokud nedojde k pádu testu). Druhá verze testovacího scénáře tedy bude vypadat následovně:
Feature: GitHub API tests @smoketest Scenario: Check the GitHub API entry point Given GitHub is accessible When I access the API endpoint / Then I should receive response with 200 status code Scenario: Check the user search feature Given GitHub is accessible When I search for user with nick torvalds Then I should receive response with 200 status code And I should receive proper JSON response And I should find the user with full name Linus Torvalds And I should find that the user works for company Linux Foundation
8. Spuštění testovacích scénářů ve chvíli, kdy nejsou implementovány všechny požadované kroky
Pokud nyní, tj. bez dalších úprav projektu, testovací scénář spustíme, vypíšou se podle očekávání zprávy o tom, které kroky zatím nejsou definovány i s nápovědou, jak je můžeme definovat (ovšem nástroj Cucumber samozřejmě neví, které části popisu testovacích scénářů obsahují proměnné, takže nápověda není úplná):
$ lein cucumber ..U.UUUUU 2 Scenarios (2 undefined) 9 Steps (6 undefined, 3 passed) 0m2.152s You can implement missing steps with the snippets below: (Then #"^I should receive response with (\d+) status code$" [arg1] (comment Write code here that turns the phrase above into concrete actions ) (throw (cucumber.api.PendingException.))) (When #"^I search for user with nick torvalds$" [] (comment Write code here that turns the phrase above into concrete actions ) (throw (cucumber.api.PendingException.))) (Then #"^I should receive proper JSON response$" [] (comment Write code here that turns the phrase above into concrete actions ) (throw (cucumber.api.PendingException.))) (Then #"^I should find the user with full name Linus Torvalds$" [] (comment Write code here that turns the phrase above into concrete actions ) (throw (cucumber.api.PendingException.))) (Then #"^I should find that the user works for company Linux Foundation$" [] (comment Write code here that turns the phrase above into concrete actions ) (throw (cucumber.api.PendingException.))) Ran 1 tests containing 1 assertions in 11 msecs 0 failures, 0 errors.
Obrázek 3: Spuštění testů ve chvíli, kdy ještě nejsou definovány všechny testovací kroky.
9. Vytvoření projektu s novými závislostmi
Implementaci výše uvedených kroků testovacího scénáře vytvoříme v dnešním třetím demonstračním projektu. Základní struktura tohoto projektu je shodná s projektem předchozím, ovšem další úpravy již budou odlišné:
├── bdd ├── doc │ └── intro.md ├── features │ ├── github.feature │ └── step_definitions │ └── github_steps.clj ├── LICENSE ├── project.clj ├── README.md └── resources 4 directories, 7 files
První podstatná změna se týká projektového souboru „project.clj“. Musíme do něj totiž přidat novou knihovnu „org.clojure/data.json“, a to pochopitelně z toho důvodu, že REST API GitHubu používá pro přenos údajů formát JSON:
(defproject cucumber+expect9 "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"] [org.clojure/data.json "0.2.5"] [expectations "2.0.9"]] :plugins [[com.siili/lein-cucumber "1.0.7"] [lein-expectations "0.0.8"]] :cucumber-feature-paths ["features/"] :target-path "target/%s" :profiles {:uberjar {:aot :all} :dev {:dependencies [[com.siili/lein-cucumber "1.0.7"]]}})
10. Implementace nových kroků vyžadovaných testovacími scénáři
Nyní budeme implementovat kód jednotlivých testovacích kroků. První část již známe, protože jsme se s ní seznámili v předchozích kapitolách. Pouze budeme muset doplnit import knihovny clojure.data/json:
(require '[expectations :refer [expect]]) (require '[clojure.data.json :as json]) (def URL "https://github.com/") (def API-URL "https://api.github.com") (def context (atom {:response nil})) (System/setProperty "https.protocols" "TLSv1,TLSv1.1,TLSv1.2") (defn request [address] (let [url (new java.net.URL address) connection (.openConnection url)] (.connect connection) {:status (.getResponseCode connection) :content (slurp (.getInputStream connection))})) (Given #"^GitHub is accessible$" [] (let [response (request URL)] (expect (:status response) 200))) (When #"^I access the API endpoint (.+)$" [endpoint] (let [response (request (str API-URL endpoint))] (swap! context assoc :response response)))
Následuje implementace nových kroků. Nejdříve se jedná o krok, v němž se pokusíme vyhledat uživatele podle jeho nicku (přezdívky). Je to snadné, protože postačuje do REST API endpointu „/users/“ poslat nick a měl by se vrátit JSON s podrobnějšími informacemi o tomto uživateli. Pro jistotu omezíme množinu znaků použitých pro zápis nicku. Odpověď, kterou získáme, uložíme bez dalšího zpracování do kontextu:
(When #"^I search for user with nick ([A-Za-z0-9]+)$" [nick] (let [response (request (str API-URL "/users/" nick))] (swap! context assoc :response response)))
Další krok s kontrolou návratového kódu již známe:
(Then #"^I should receive response with (\d+) status code$" [code] (let [expected_code (Integer/parseInt code) actual_code (-> @context :response :status)] (expect expected_code actual_code)))
Následuje test, který zjišťuje, zda se v těle odpovědi skutečně nachází data ve formátu JSON. V tomto případě se pouze pokusíme o parsing dat se zahozením výsledku (parsing není „línou“ funkcí, takže by se skutečně měl provést):
(Then #"^I should receive proper JSON response$" [] (let [content (-> @context :response :content)] (json/read-str content)))
Poslední dva testovací kroky zjišťují informace uložené pod klíči „name“ a „company“ v datové struktuře získané z JSON dat. Připomeňme si, že funkci get je možné použít i ve chvíli, kdy příslušný klíč neexistuje:
(Then #"^I should find the user with full name (.+)$" [full-name] (let [content (-> @context :response :content json/read-str)] (expect (get content "name") full-name))) (Then #"^I should find that the user works for company (.+)$" [company-name] (let [content (-> @context :response :content json/read-str)] (expect (get content "company") company-name)))
Poznámka: efektivnější by bylo uložit výsledek parsingu JSONu do kontextu již v kroku „I should receive proper JSON response“.
11. Spuštění testovacích scénářů kompletního projektu
Nyní je již vše připravené pro spuštění nového testovacího scénáře, takže si ho zkusme spustit:
$ lein cucumber Running cucumber... Looking for features in: [/home/tester/temp/clojure/clojure-examples/cucumber+expect9/features] Looking for glue in: [/home/tester/temp/clojure/clojure-examples/cucumber+expect9/features/step_definitions] ......... 2 Scenarios (2 passed) 9 Steps (9 passed) 0m2.418s Ran 4 tests containing 4 assertions in 15 msecs 0 failures, 0 errors.
Obrázek 4: Spuštění kompletního testovacího scénáře.
12. Vylepšení testovacího scénáře – vyhledání informací o větším množství uživatelů
Připomeňme si, že jednou z velmi zajímavých možností, jakými je možné testovací scénáře rozšířit, spočívá v tom, že se specifikuje tabulka či tabulky se vstupními hodnotami a očekávanými výsledky. Takovou tabulku si samozřejmě můžeme připravit i pro test s vyhledáním uživatelů na GitHubu:
Nick | Plné jméno | Firma/organizace |
---|---|---|
torvalds | Linus Torvalds | Linux Foundation |
brammool | Bram Moolenaar | Zimbu Labs |
tisnik | Pavel Tišnovský | Red Hat, Inc. |
(nemohl jsem odolat a přidal jsem se za oba velikány :-) Je tomu tak z toho důvodu, že potřebujeme otestovat i znaky s nabodeníčky)
Testovací scénář se již upraví snadno. Jen si připomeňme, že se musí použít Scenario Outline a ty části vět z testů, které se mají nahradit hodnotami z tabulky, se musí uzavřít do úhlových závorek:
Feature: GitHub API tests @smoketest Scenario: Check the GitHub API entry point Given GitHub is accessible When I access the API endpoint / Then I should receive response with 200 status code Scenario: Check the user search feature Given GitHub is accessible When I search for user with nick torvalds Then I should receive response with 200 status code And I should receive proper JSON response And I should find the user with full name Linus Torvalds And I should find that the user works for company Linux Foundation Scenario Outline: Check the user search feature, perform the search for more users Given GitHub is accessible When I search for user with nick <nick> Then I should receive response with 200 status code And I should receive proper JSON response And I should find the user with full name <fullname> And I should find that the user works for company <company> Examples: users |nick|fullname|company| |torvalds|Linus Torvalds|Linux Foundation| |brammool|Bram Moolenaar|Zimbu Labs| |tisnik|Pavel Tišnovský|Red Hat, Inc.|
13. Opětovné spuštění testovacích scénářů
Po spuštění takto upravených testů získáme již očekávané výsledky. Pouze bych upozornil na celkový počet kroků, který zde dosahuje hodnoty 27. Je tomu tak pochopitelně z toho důvodu, že se všechny kroky v posledním scénáři opakují třikrát, pokaždé pro jiný nick:
$ lein cucumber Running cucumber... Looking for features in: [/home/tester/temp/clojure/clojure-examples/cucumber+expect9/features] Looking for glue in: [/home/tester/temp/clojure/clojure-examples/cucumber+expect9/features/step_definitions] ......... ........................... 5 Scenarios (5 passed) 27 Steps (27 passed) 0m5.025s Ran 4 tests containing 4 assertions in 13 msecs 0 failures, 0 errors.
14. Malá úprava testů pro uživatele GitHubu bez vyplněné společnosti
Někteří uživatelé přihlášení na GitHubu nemají vyplněnou společnost. To je kupodivu případ i Riche Hickeyho – autora programovacího jazyka Clojure i dalších zajímavých projektů. Nejdříve tedy Riche přidáme do tabulky v testovacím scénáři:
Feature: GitHub API tests @smoketest Scenario: Check the GitHub API entry point Given GitHub is accessible When I access the API endpoint / Then I should receive response with 200 status code Scenario: Check the user search feature Given GitHub is accessible When I search for user with nick torvalds Then I should receive response with 200 status code And I should receive proper JSON response And I should find the user with full name Linus Torvalds And I should find that the user works for company Linux Foundation Scenario Outline: Check the user search feature, perform the search for more users Given GitHub is accessible When I search for user with nick Then I should receive response with 200 status code And I should receive proper JSON response And I should find the user with full name And I should find that the user works for company Examples: users |nick|fullname|company| |torvalds|Linus Torvalds|Linux Foundation| |brammool|Bram Moolenaar|Zimbu Labs| |richhickey|Rich Hickey|| |tisnik|Pavel Tišnovský|Red Hat, Inc.|
A následně nepatrně upravíme příslušný testovací krok takovým způsobem, aby se vrátil prázdný řetězec pro ty uživatele GitHubu, kteří nemají firmu vyplněnou. Můžeme využít toho, že funkci get lze předat třetí nepovinný parametr s výchozí hodnotou:
(Then #"^I should find that the user works for company (.*)$" [company-name] (let [content (-> @context :response :content json/read-str)] (expect (get content "company" "") company-name)))
15. Testovací scénář použitelný pro libovolné REST API, nejenom pro GitHub
Na závěr provedeme ještě jedno vylepšení testovacího scénáře. Do věty začínající na Given vložíme jak jméno testované služby, tak i její adresu. Tyto informace se tedy budou moci stát součástí kontextu testů (viz zvýrazněné části obsahující modifikovatelný text):
Feature: GitHub API tests @smoketest Scenario: Check the GitHub API entry point Given REST API for GitHub service is accessible on URL https://api.github.com When I access the API endpoint / Then I should receive response with 200 status code Scenario: Check the user search feature Given REST API for GitHub service is accessible on URL https://api.github.com When I search for user with nick torvalds Then I should receive response with 200 status code And I should receive proper JSON response And I should find the user with full name Linus Torvalds And I should find that the user works for company Linux Foundation Scenario Outline: Check the user search feature, perform the search for more users Given REST API for GitHub service is accessible on URL https://api.github.com When I search for user with nick Then I should receive response with 200 status code And I should receive proper JSON response And I should find the user with full name And I should find that the user works for company Examples: users |nick|fullname|company| |torvalds|Linus Torvalds|Linux Foundation| |brammool|Bram Moolenaar|Zimbu Labs| |richhickey|Rich Hickey|| |tisnik|Pavel Tišnovský|Red Hat, Inc.|
16. Úprava kroků testu
Začátek skriptu s testovacími kroky se bude muset nepatrně změnit, protože budeme potřebovat do kontextu přidat i URL testované služby (klidně si můžeme přidat i její jméno):
(require '[expectations :refer [expect]]) (require '[clojure.data.json :as json]) (def context (atom {:response nil :api-url nil}))
Následuje původní část:
(System/setProperty "https.protocols" "TLSv1,TLSv1.1,TLSv1.2") (defn request [address] (let [url (new java.net.URL address) connection (.openConnection url)] (.connect connection) {:status (.getResponseCode connection) :content (slurp (.getInputStream connection))}))
V kroku Given si zapamatujeme URL služby a následovně se ji pokusíme zkontaktovat (většina REST API služeb by měla zareagovat buď kódem 200 nebo 3×x pro přesměrování):
(Given #"^REST API for ([A-Za-z]+) service is accessible on URL (.*)$" [service url] (swap! context assoc :api-url url) (let [api-url (:api-url @context) response (request (:api-url @context))] (expect (:status response) 200)))
Další dva kroky již budou využívat URL uložené do kontextu:
(When #"^I access the API endpoint (.+)$" [endpoint] (let [api-url (:api-url @context) response (request (str api-url endpoint))] (swap! context assoc :response response))) (When #"^I search for user with nick ([A-Za-z0-9]+)$" [nick] (let [api-url (:api-url @context) response (request (str api-url "/users/" nick))] (swap! context assoc :response response)))
Zajímavé je, že všechny další kroky (Then, And) již URL nepotřebují a tedy se ani jejich kód žádným způsobem nemusel modifikovat:
(Then #"^I should receive response with (\d+) status code$" [code] (let [expected_code (Integer/parseInt code) actual_code (-> @context :response :status)] (expect expected_code actual_code))) (Then #"^I should receive proper JSON response$" [] (let [content (-> @context :response :content)] (json/read-str content))) (Then #"^I should find the user with full name (.+)$" [full-name] (let [content (-> @context :response :content json/read-str)] (expect (get content "name") full-name))) (Then #"^I should find that the user works for company (.*)$" [company-name] (let [content (-> @context :response :content json/read-str)] (expect (get content "company" "") company-name)))
17. Repositář s demonstračními příklady
Všech šest demonstračních příkladů a projektů určených pro programovací jazyk Clojure verze 1.9.0 popř. Clojure 1.8.0 bylo uloženo do repositáře, který naleznete na adrese https://github.com/tisnik/clojure-examples:
# | Projekt | Odkaz |
---|---|---|
1 | cucumber+expect7 | https://github.com/tisnik/clojure-examples/tree/master/cucumber+expect7 |
2 | cucumber+expect8 | https://github.com/tisnik/clojure-examples/tree/master/cucumber+expect8 |
3 | cucumber+expect9 | https://github.com/tisnik/clojure-examples/tree/master/cucumber+expect9 |
4 | cucumber+expectA | https://github.com/tisnik/clojure-examples/tree/master/cucumber+expectA |
5 | cucumber+expectB | https://github.com/tisnik/clojure-examples/tree/master/cucumber+expectB |
6 | cucumber+expectC | https://github.com/tisnik/clojure-examples/tree/master/cucumber+expectC |
18. 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/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/
19. 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