(Это новый подход, без eval
- и binding
. Как обсуждалось в
комментарии к этому ответу, использование eval
проблематично, потому что
это предотвращает закрытие тестов над лексической средой, которой они кажутся
определяется в (т. е. (let [x 1] (deftest easy (is (= x 1))))
нет
дольше работает). Я оставляю оригинальный подход в нижней половине
ответ, ниже горизонтального правила.)
macrolet
подход
Осуществление
Протестировано с Clojure 1.3.0-бета2; вероятно, он должен работать с 1.2.x как
хорошо.
(ns deftest-magic.core
(:use [clojure.tools.macro :only [macrolet]]))
(defmacro with-test-tags [tags & body]
(let [deftest-decl
(list 'deftest ['name '& 'body]
(list 'let ['n `(vary-meta ~'name update-in [:tags]
(fnil into #{}) ~tags)
'form `(list* '~'clojure.test/deftest ~'n ~'body)]
'form))
with-test-tags-decl
(list 'with-test-tags ['tags '& 'body]
`(list* '~'deftest-magic.core/with-test-tags
(into ~tags ~'tags) ~'body))]
`(macrolet [~deftest-decl
~with-test-tags-decl]
~@body)))
Использование
... лучше всего продемонстрировать набор (проходящих) тестов:
(ns deftest-magic.test.core
(:use [deftest-magic.core :only [with-test-tags]])
(:use clojure.test))
;; defines a test with no tags attached:
(deftest plain-deftest
(is (= :foo :foo)))
(with-test-tags #{:foo}
;; this test will be tagged #{:foo}:
(deftest foo
(is true))
(with-test-tags #{:bar}
;; this test will be tagged #{:foo :bar}:
(deftest foo-bar
(is true))))
;; confirming the claims made in the comments above:
(deftest test-tags
(let [plaintest-tags (:tags (meta #'plain-deftest))]
(is (or (nil? plaintest-tags) (empty? plaintest-tags))))
(is (= #{:foo} (:tags (meta #'foo))))
(is (= #{:foo :bar} (:tags (meta #'foo-bar)))))
;; tests can be closures:
(let [x 1]
(deftest lexical-bindings-no-tags
(is (= x 1))))
;; this works inside with-test-args as well:
(with-test-tags #{:foo}
(let [x 1]
(deftest easy (is true))
(deftest lexical-bindings-with-tags
(is (= #{:foo} (:tags (meta #'easy))))
(is (= x 1)))))
Примечания к дизайну:
Мы хотим сделать дизайн на основе macrolet
, описанный в
работа над текстом вопроса. Мы заботимся о возможности гнездиться
with-test-tags
и сохранение возможности определения тестов
чьи тела близко к лексической среде они определены
в.
Мы будем macrolet
TING deftest
, чтобы расширить до
clojure.test/deftest
форма с соответствующими метаданными, прикрепленными к
название теста. Важной частью здесь является то, что with-test-tags
вставляет соответствующий набор тегов прямо в определение
пользовательский локальный deftest
внутри формы macrolet
; однажды
компилятор приступает к расширению форм deftest
, наборы тегов
будет встроен в код.
Если оставить все как есть, тесты, определенные внутри вложенного
with-test-tags
будет помечен только тегами, переданными
внутренняя with-test-tags
форма. Таким образом, мы также имеем with-test-tags
macrolet
символ with-test-tags
сам ведёт себя очень похоже
местный deftest
: расширяется до вызова на высшем уровне
with-test-tags
макрос с соответствующими тегами, введенными в
множества ярлыков.
Намерение состоит в том, что внутренняя with-test-tags
форма в
(with-test-tags #{:foo}
(with-test-tags #{:bar}
...))
развернуть до (deftest-magic.core/with-test-tags #{:foo :bar} ...)
(если действительно deftest-magic.core
это пространство имен with-test-tags
определяется в). Эта форма сразу превращается в знакомую
Форма macrolet
, с символами deftest
и with-test-tags
локально привязан к макросам с правильными наборами тегов, встроенными внутри
их.
(Оригинальный ответ обновлен некоторыми примечаниями по дизайну, некоторые
перефразирование, переформатирование и т. д. Код не изменился.)
Подход binding
+ eval
.
(см. Также https://gist.github.com/1185513 для версии
дополнительно используя macrolet
, чтобы избежать пользовательского верхнего уровня
deftest
.)
Осуществление
Следующие тестируются для работы с Clojure 1.3.0-бета2; с
^:dynamic
часть удалена, она должна работать с 1.2:
(ns deftest-magic.core)
(def ^:dynamic *tags* #{})
(defmacro with-test-tags [tags & body]
`(binding [*tags* (into *tags* ~tags)]
~@body))
(defmacro deftest [name & body]
`(let [n# (vary-meta '~name update-in [:tags] (fnil into #{}) *tags*)
form# (list* 'clojure.test/deftest n# '~body)]
(eval form#)))
Использование
(ns example.core
(:use [clojure.test :exclude [deftest]])
(:use [deftest-magic.core :only [with-test-tags deftest]]))
;; defines a test with an empty set of tags:
(deftest no-tags
(is true))
(with-test-tags #{:foo}
;; this test will be tagged #{:foo}:
(deftest foo
(is true))
(with-test-tags #{:bar}
;; this test will be tagged #{:foo :bar}:
(deftest foo-bar
(is true))))
Примечания к дизайну
Я думаю, что в этом случае разумное использование eval
приводит к
полезное решение. Базовый дизайн (на основе "binding
-able Var"
идея) состоит из трех компонентов:
Динамически связываемый Var - *tags*
- который связывается при компиляции
время для набора тегов, которые будут использоваться deftest
формами для украшения
тесты определяются. Мы не добавляем теги по умолчанию, поэтому его начальный
значение #{}
.
Макрос with-test-tags
, который устанавливает соответствующий для
*tags*
.
Пользовательский макрос deftest
, который расширяется до let
формы, напоминающей
это (следующее расширение, слегка упрощенное для
ясность):
(let [n (vary-meta '<NAME> update-in [:tags] (fnil into #{}) *tags*)
form (list* 'clojure.test/deftest n '<BODY>)]
(eval form))
<NAME>
и <BODY>
- аргументы, переданные обычаю
deftest
, вставленный в соответствующие места через кавычки
соответствующие части шаблона расширения с синтаксическими кавычками.
Таким образом, расширение пользовательского deftest
представляет собой let
форму, в которой
во-первых, название нового теста готовится путем украшения
символ с метаданными :tags
; тогда форма clojure.test/deftest
используя это украшенное имя построено; и, наконец, последняя форма
вручается eval
.
Ключевым моментом здесь является то, что выражения (eval form)
здесь
оценивается всякий раз, когда пространство имен, в котором они содержатся, является AOT-скомпилированным или
требуется в первый раз во время жизни JVM, работающей с этим
код. Это точно так же, как (println "asdf")
вверхний уровень (def asdf (println "asdf"))
, который будет печатать asdf
всякий раз, когда пространство имен AOT-компилируется или требуется в первый раз ;на самом деле, (println "asdf")
верхнего уровня действует аналогично.
Это объясняется тем, что в Clojure компиляция - это просто оценка всех форм верхнего уровня.В (binding [...] (deftest ...)
, binding
является формой верхнего уровня, но она возвращается только тогда, когда deftest
делает, и наш пользовательский deftest
расширяется до формы, которая возвращается, когда eval
.(С другой стороны, способ require
выполняет код верхнего уровня в уже скомпилированных пространствах имен - так что если в вашем коде есть (def t
(System/currentTimeMillis))
, значение t
будет зависеть от того, когда вам требуется ваше пространство имен, а нечем когда он был скомпилирован, что можно определить, экспериментируя с AOT-скомпилированным кодом - это именно то, как работает Clojure. Используйте read-eval, если хотите, чтобы в код были встроены фактические константы.пользовательский deftest
запускает компилятор (через eval
) во время выполнения макрокоманды во время компиляции.Fun.
Наконец, когда форма deftest
помещается внутри формы with-test-tags
, form
из (eval form)
будет подготовлен с привязками, установленными with-test-tags
на месте.Таким образом, определяемый тест будет украшен соответствующим набором тегов.
На REPL
user=> (use 'deftest-magic.core '[clojure.test :exclude [deftest]])
nil
user=> (with-test-tags #{:foo}
(deftest foo (is true))
(with-test-tags #{:bar}
(deftest foo-bar (is true))))
#'user/foo-bar
user=> (meta #'foo)
{:ns #<Namespace user>,
:name foo,
:file "NO_SOURCE_PATH",
:line 2,
:test #<user$fn__90 user$fn__90@50903025>,
:tags #{:foo}} ; <= note the tags
user=> (meta #'foo-bar)
{:ns #<Namespace user>,
:name foo-bar,
:file "NO_SOURCE_PATH",
:line 2,
:test #<user$fn__94 user$fn__94@368b1a4f>,
:tags #{:foo :bar}} ; <= likewise
user=> (deftest quux (is true))
#'user/quux
user=> (meta #'quux)
{:ns #<Namespace user>,
:name quux,
:file "NO_SOURCE_PATH",
:line 5,
:test #<user$fn__106 user$fn__106@b7c96a9>,
:tags #{}} ; <= no tags works too
И просто для уверенности, что рабочие тесты определены ...
user=> (run-tests 'user)
Testing user
Ran 3 tests containing 3 assertions.
0 failures, 0 errors.
{:type :summary, :pass 3, :test 3, :error 0, :fail 0}