{"id":16726317,"url":"https://github.com/brunobonacci/rdt","last_synced_at":"2025-07-17T22:34:33.440Z","repository":{"id":46490937,"uuid":"398637803","full_name":"BrunoBonacci/rdt","owner":"BrunoBonacci","description":"RDT is a REPL-Driven Test library for Clojure","archived":false,"fork":false,"pushed_at":"2024-02-01T19:16:45.000Z","size":92,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-22T16:58:55.047Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/BrunoBonacci.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-21T19:10:21.000Z","updated_at":"2023-08-02T14:30:44.000Z","dependencies_parsed_at":"2024-02-01T20:44:06.114Z","dependency_job_id":null,"html_url":"https://github.com/BrunoBonacci/rdt","commit_stats":{"total_commits":68,"total_committers":2,"mean_commits":34.0,"dds":0.05882352941176472,"last_synced_commit":"c0ef9c40759668d4610df0fc578d0d5730959b3c"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/BrunoBonacci/rdt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BrunoBonacci%2Frdt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BrunoBonacci%2Frdt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BrunoBonacci%2Frdt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BrunoBonacci%2Frdt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/BrunoBonacci","download_url":"https://codeload.github.com/BrunoBonacci/rdt/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/BrunoBonacci%2Frdt/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265672369,"owners_count":23808845,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-12T22:52:51.446Z","updated_at":"2025-07-17T22:34:33.421Z","avatar_url":"https://github.com/BrunoBonacci.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RDT - REPL-Driven tests\n[![Clojars Project](https://img.shields.io/clojars/v/com.brunobonacci/rdt.svg)](https://clojars.org/com.brunobonacci/rdt)  [![cljdoc badge](https://cljdoc.org/badge/com.brunobonacci/rdt)](https://cljdoc.org/d/com.brunobonacci/rdt/CURRENT) [![CircleCI](https://circleci.com/gh/BrunoBonacci/rdt/tree/main.svg?style=shield)](https://circleci.com/gh/BrunoBonacci/rdt/tree/main) ![last-commit](https://img.shields.io/github/last-commit/BrunoBonacci/rdt.svg)\n\nRDT is a REPL-Driven Test library for Clojure. It enables you to write\ntests that look like REPL sessions. It makes use of pattern matching and\nother functionalities to make the tests more readable.\n\nRDT is a library designed to create tests that are like REPL sessions,\nlow structure, easy to debug when they fail because they are just REPL\nsessions. _This library favours tests readability over design\npurity._\n\nRDT is heavily inspired on **Midje** adding a couple of features for\ncode readability..\n\n**WORK IN PROGRESS:** Some behaviours might change.\n\n\n## Motivation\n\nI strongly believe that tests should be \"obvious\" to the reader. I've\nseen tests that were more complex than the code they were testing. I\nbelieve that libraries such as [Midje](https://github.com/marick/Midje)\nmake a good attempt to make tests more readable to the developer. I\nthink Midje is largely underappreciated in our community.\n\nMost of the testing libraries have a strong focus on unit-tests, which\nin a functional language like Clojure tend to be pure functions which\nare stateless.\n\nSo the general pattern is something like:\n\n``` clojure\n;; midje tests example\n(fact \"some statement\"\n  (function arg1 arg2 arg3) =\u003e expected-result)\n```\n\nHowever, when tests are stateful, such as most of the integration tests,\nthe test starts to be more and more complex.\n\nThe following example is a test which verifies the correct functionality\nof a queueing system.\n\n``` clojure\n;; midje tests example\n(fact \"items sent to a queue can be retrieved from the same\"\n\n  (let [cfg    (start-test-cluster)\n        q-name (str (java.util.UUID/randomUUID))\n        queue  (create-queue cfg q-name)]\n\n    (pop-items cfg queue) =\u003e empty?\n\n    (let [item {:some :message :random (rand)}\n          msg  (send cfg queue item)]\n\n      msg =\u003e (contains {:message-id string?})\n\n      (let [q-item (first (pop-items cfg queue))]\n\n        q-item =\u003e (contains {:message-id (:message-id msg)\n                             :item item})\n\n        (delete-item cfg queue q-item) =\u003e (contains {:status :deleted})))))\n```\n\nThis test is a simplified version of a real-world test, however it is\nalready explicative of the problem faced.\n\nAssume that that this test is failing, in order to debug you will need\nto start the REPL and evaluate each expression in order. However,\nbecause of the nested `let` expressions, you will need to **unnest** it\nfirst, which is basically rewriting the whole test in the repl.\n\nLet's see the same test written in RDT:\n\n``` clojure\n;; with RDT\n(repl-test \"items sent to a queue can be retrieved from the same\"\n\n  (def cfg    (start-test-cluster))\n  (def q-name (str (java.util.UUID/randomUUID)))\n  (def queue  (create-queue cfg q-name))\n\n  (pop-items cfg queue) =\u003e empty?\n\n  (def item {:some :message :random (rand)})\n  (def msg  (send cfg queue item))\n\n  msg =\u003e {:message-id string?}\n\n  (def q-item (first (pop-items cfg queue)))\n\n  q-item =\u003e {:message-id (:message-id msg)\n             :item item}\n\n  (delete-item cfg queue q-item) =\u003e {:status :deleted})\n```\n\nI don't think there is any doubt about which one of the two tests it is\neasier to read and understand what is happening. Moreover, if the test\nfails, it is extremely easy to start the REPL and evaluate each\nexpression to debug and understand where is the problem.\n\nBehind the scenes, RDT converts the test from the second form to the\nfirst form, so the two tests are equivalent, all the `def` are valid\nwithin the scope of `repl-test` only.\n\nFor example, the following snippet illustrates this point.\n\n``` clojure\n\n(repl-test \"scoping rules for def and defn\"\n\n  (def  a 1)\n  (def  b 2)\n  (defn sum [x y] (+ x y))\n\n  (sum a b) =\u003e 3\n\n  (println \"a:\" a \", b:\" b \", sum:\" sum)\n  ;; prints:\n  ;; a: 1 , b: 2 , sum: #function[user/eval36636/fn--36637/fn--36638/sum--36640]\n)\n\n;; `a`, `b` and `sum` do not exists outside of the `repl-test` scope\n;; (println \"a:\" a \", b:\" b \", sum:\" sum)\n;; Syntax error\n;; Unable to resolve symbol: a in this context\n\n```\n\nInternally the `def` and `defn` are converted into `let` binding\nthus avoiding the concurrency problems with the use of `def`.\n\n\n## Usage\n\nIn order to use the library add the dependency to your `project.clj`\n\n``` clojure\n;; Leiningen project\n[com.brunobonacci/rdt \"0.5.0-alpha1\"]\n\n;; deps.edn format\n{:deps { com.brunobonacci/rdt {:mvn/version \"0.5.0-alpha1\"}}}\n```\n\nCurrent version:\n[![](https://img.shields.io/clojars/v/com.brunobonacci/rdt.svg)](https://clojars.org/com.brunobonacci/rdt)\n\nThen require the namespace:\n\n``` clojure\n(ns foo.bar\n  (:require [com.brunobonacci.rdt :refer [repl-test]]))\n```\n\nCheck the [online\ndocumentation](https://cljdoc.org/d/com.brunobonacci/rdt/CURRENT)\n\n### Writing tests\n\n\nThe general form is:\n\n``` clojure\n(repl-test \"description of what you want to test (optional)\"\n\n    (function1 arg1 arg2 arg3) =\u003e expected-value1\n\n    (function2 arg1 arg2 arg3) =\u003e expected-value2\n\n    (functionN arg1 arg2 arg3) =\u003e expected-valueN\n )\n```\n\nThe left side of the arrow `=\u003e` is the function you want to test, the\nright side of the arrow is the value or pattern you expect.\n\n\n#### Finalizers\n\nOftentimes at the end of your test you need to run a function\nto cleanup after the test execution whether the test was successful\nor not. For this case we have the finalizers support:\n\n``` clojure\n(repl-test \"test with finalizer\"\n\n    (def services (starup-services {}))\n\n    (function1 services arg2 arg3) =\u003e expected-value1\n\n    (function2 services arg2 arg3) =\u003e expected-value2\n\n    (functionN services arg2 arg3) =\u003e expected-valueN\n\n    ;; add your finalization code after the `:rdt/finalize` keyword\n    ;; the following code will be executed in any case.\n    :rdt/finalize\n    (shutdown services)\n    (cleanup  services)\n )\n```\n\nJust add the `:rdt/finalize` followed by the expressions which will be\nevaluated at the end of the test execution, whether the test was\nsuccessful or not.\n\n\n### Fuzzy matching arrow `=\u003e`\n\n\nThe single arrow (`=\u003e`) performs a fuzzy match of the left-hand-side of\nthe arrow with the right-hand-side of the arrow. It works differently\nthan Midje.\n\nThe fuzzy match work as follow:\n\n#### Basic values are matched with equality (`=`)\n\n``` clojure\n(repl-test\n  (reduce + (range 1000)) =\u003e 499500    ;; numbers\n  (str \"foo\" \"-\" \"bar\")   =\u003e \"foo-bar\" ;; strings\n  (number? 23)            =\u003e true      ;; boolean\n  (keyword \"foo\")         =\u003e :foo      ;; keywords\n  (quote foo)             =\u003e 'foo      ;; symbols\n  (first [])              =\u003e nil       ;; nil\n  )\n```\n\n#### Lists, Vectors and Primitive arrays are matched element-by-element\n\n``` clojure\n(repl-test\n  (vector)       =\u003e []    ;; true\n  (vector 1)     =\u003e [1]   ;; true\n  (vector 2)     =\u003e [1]   ;; false, test fails\n  (vector 1 2)   =\u003e [1 3] ;; false, test fails\n  (vector 1 2 3) =\u003e [1 2] ;; false, test fails\n\n  (list)         =\u003e '()   ;; true\n  (list 1)       =\u003e '(1)  ;; true\n  (list 2)       =\u003e '(1)  ;; false, test fails\n  (list 1 2)     =\u003e '(1 3);; false, test fails\n  (list 1 2 3)   =\u003e '(1 2);; false, test fails\n\n  (byte-array 0)       =\u003e []    ;; true\n  (byte-array [1])     =\u003e [1]   ;; true\n  (byte-array [2])     =\u003e [1]   ;; false, test fails\n  (byte-array [1 2])   =\u003e [1 3] ;; false, test fails\n  (byte-array [1 2 3]) =\u003e [1 2] ;; false, test fails\n)\n```\n\n#### Maps are matched only on the given keys\n\nMaps are matched only on the subset of keys which are present on the\nright-hand-side, the actual map can have more keys.\n\n``` clojure\n(repl-test\n  (conj  {} :s 2)            =\u003e {:s 2}      ;; true\n  (merge {:s 2} {:b 3}       =\u003e {:s 2}      ;; true, the map contain the key :s with the value 2\n  (conj  {:s 2 :b 3} [:x 4]) =\u003e {:s 2 :x 4} ;; true, all required keys and values are there\n  (conj  {:s 2 :b 3} [:Z 4]) =\u003e {:s 2 :x 4} ;; false, key :x is missing\n)\n```\n\n#### Sets are matched based on the given subset of values\n\n``` clojure\n(repl-test\n  (set 1 2 3)  =\u003e #{1 3 2} ;; true, the sets are the same\n  (set 1 2 3)  =\u003e #{1 3}   ;; true, #{1 3} is a subset of #{1 2 3}\n  (set 1 2 3)  =\u003e #{1 4}   ;; false, #{1 4} is NOT a subset of #{1 2 3}\n)\n```\n\n#### Match against functions\n\nIn the right-hand-side you can use functions in place of values, in that\ncase the function will be applied to the value and check whether the\nresult is true or false.\n\n``` clojure\n(repl-test\n  (+ 1 2 3)                   =\u003e number?  ;; true\n  {:num 1 :str \"two\"}         =\u003e map?     ;; true\n  {:num 1 :str \"two\" :k :z}   =\u003e {:num odd? :str string?} ;; all true,\n)\n```\n\n#### Regex to match strings\n\nIn the right-hand-side you can use regular expression patterns in place\nof values, in that case the pattern will be matched against the value\nand check whether the result is true or false.\n\n``` clojure\n(repl-test\n  (str \"foo\" 123) =\u003e #\"foo.*\"       ;; true\n  (str \"foo\" 123) =\u003e #\"foo\"         ;; false, not a full match\n  (str \"foo\" 123) =\u003e #\"(?i)^FO+\\d+\" ;; true\n  )\n```\n\n### Why the fuzzy matcher?\n\n\nIntegration tests often return big nested maps with many keys, many of\nwhich values represent system state. On the other hand, your tests are\noften only focused on verifying that only specific keys have a\nparticular value. The fuzzy matching is very useful in that case, as it\nonly will match against the pattern you provided.\n\nFor example assume you have a function which returns a map with many\nnested keys and you are interested on testing only a few specific keys\non the nested map, then the fuzzy match helps very much with it and\nallows for a very readable test.\n\nFor example assume you have a function which returns the current product\navailability if your depots:\n\n``` clojure\n(product-availability \"SKU123ABZ\")\n;;=\u003e {:product-id  \"SKU123ABZ\"\n;;    :description \"Next generation 3D printer\"\n;;    :availability\n;;    {:free-stock {\"LON1\" {:location-id \"LON1\"\n;;                          :description \"London depot\"\n;;                          :quantity    13}\n;;                  \"BER1\" {:location-id \"BER1\"\n;;                          :description \"Berlin depot\"\n;;                          :quantity    3}}\n;;     :reserved   {\"LON1\" 0\n;;                  \"BER1\" 0}}}\n```\n\nLet's assume you want to test a function which reverses some stock, if\navailable, then you can write a test as follow:\n\n``` clojure\n(repl-test \"Stock reservation\"\n\n  (reserve-stock \"SKU123ABZ\" \"LON1\" 10)\n  =\u003e {:product-id  \"SKU123ABZ\"\n      :availability\n      {:free-stock {\"LON1\" {:quantity 3}}\n       :reserved   {\"LON1\" 10}}}\n  )\n```\n\nAs you see in the above test you can specify only the subset of the map\nwhich is relevant to your test and ignore the rest. So much better for\nreadability.\n\n### What if I need more precise matching?\n\n\nIf you need an exact matching or something in between the exact matching\nand the fuzzy matching then you can use the **double-equal arrow** `==\u003e`\nwhich behaves like Midje `=\u003e` arrow.\n\n``` clojure\n;; same behaviour as Midje arrow =\u003e\n(repl-test \"Stock reservation\"\n\n  (reserve-stock \"SKU123ABZ\" \"LON1\" 10)\n  ==\u003e (contains\n        {:product-id  \"SKU123ABZ\"\n         :availability\n         (contains\n           {:free-stock (contains {\"LON1\" (contains {:quantity 3})})\n            :reserved   (contains {\"LON1\" 10})})})\n  )\n```\n\n\n## Test labels and test filters\n\nIf you want selectively run or skip tests you can give them labels and\nuse the labels to filter or skip some test.\n\n``` Clojure\n\n(repl-test {:labels [:core]} \"important core tests\"\n\n    ;; things that you must always run\n)\n\n(repl-test {:labels [:slow]} \"compute pi to 1 trillion decimal values\"\n\n    ;; compute pi\n)\n\n\n(repl-test {:labels [:core :integration]} \"Check DB transaction isolation \"\n\n    ;; start DB container\n    ;; connect to DB\n    ;; run transactions\n    ;; verify isonlation\n    ;; shutdown services\n)\n```\n\nThen you can selectively run the tests you want or skip them.\nFor example:\n\n\n``` bash\n# run only :core tests\nlein run '{:include-labels [:core]}'\n\n# skip :slow tests\nlein run '{:exclude-labels [:slow]}'\n\n\n# run :core tests, but skip integration\nlein run '{:include-labels [:core] :exclude-labels [:integration]}'\n```\n\n## License\n\nCopyright © 2021-2022 Bruno Bonacci - Distributed under the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrunobonacci%2Frdt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrunobonacci%2Frdt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrunobonacci%2Frdt/lists"}