diff --git a/CHANGELOG.md b/CHANGELOG.md index 87cceaba..cffc8259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + * Added functions to `basilisp.test` for using and combining test fixtures (#980) + ### Fixed * Fix a bug where the reader was double counting the CRLF newline seq in metadata (#1063) * Conform to the `cider-nrepl` `info` ops spec by ensuring result's `:file` is URI, also added missing :column number (#1066) diff --git a/src/basilisp/test.lpy b/src/basilisp/test.lpy index 4379d016..a80be22d 100644 --- a/src/basilisp/test.lpy +++ b/src/basilisp/test.lpy @@ -19,6 +19,7 @@ Tests may take advantage of Basilisp fixtures via :lpy:fn:`use-fixtures` to perform setup and teardown functions for each test or namespace. Fixtures are not the same (nor are they compatible with) PyTest fixtures." + (:import types) (:require [basilisp.template :as template])) @@ -59,6 +60,11 @@ via ``use-fixture`` and then removing them after the test is defined." (fn [fixture-type & _] fixture-type)) +(defn generator? + "Return true if ``x`` is a generator type, else false." + [x] + (instance? types/GeneratorType x)) + (defmethod use-fixtures :each [_ & fixtures] (alter-meta! *ns* assoc ::each-fixtures fixtures)) @@ -67,6 +73,42 @@ [_ & fixtures] (alter-meta! *ns* assoc ::once-fixtures fixtures)) +(defmacro with-fixtures + "Wrap the ``body`` in the ``fixtures`` in the given order. Handle + setup and teardown for each of the ``fixtures``." + [fixtures & body] + (assert (vector? fixtures) "Expected a literal vector of fixtures") + (let [result (gensym "result")] + (reduce (fn [form fixture] + `(let [~result (~fixture)] + (if (generator? ~result) + (try + (python/next ~result) + ~form + (finally + (try + (python/next ~result) + (catch python/StopIteration ~'_ nil)))) + ~form))) + `(do ~@body) + (reverse fixtures)))) + +(defmacro compose-fixtures + "Compose any number of ``fixtures``, in order, creating a new fixture + that combines their behavior. Always returns a valid fixture + function, even if no fixtures are given." + [& fixtures] + `(fn [] (with-fixtures [~@fixtures] (yield)))) + +(defn join-fixtures + "Composes a collection of ``fixtures``, in order. Always returns a valid + fixture function, even if the collection is empty. + Prefer :lpy:fn:`compose-fixtures` if fixtures are known at compile time." + [fixtures] + (if (seq fixtures) + (reduce #(compose-fixtures %1 %2) fixtures) + (constantly nil))) + (defmulti ^{:arglists '([expr msg line-num])} gen-assert diff --git a/tests/basilisp/contrib/nrepl_server_test.lpy b/tests/basilisp/contrib/nrepl_server_test.lpy index 1856d58b..2b5eb4a6 100644 --- a/tests/basilisp/contrib/nrepl_server_test.lpy +++ b/tests/basilisp/contrib/nrepl_server_test.lpy @@ -273,29 +273,36 @@ {:id @id* :ns "user" :value "1"} {:id @id* :ns "user" :status ["done"]}) (client-send! client {:id (id-inc!) :op "complete" :prefix "clojure.test/"}) - (is (= {:id @id* :status ["done"] - :completions [{:type "var" :ns "basilisp.test" :candidate "clojure.test/*test-failures*"} - {:type "var" :ns "basilisp.test" :candidate "clojure.test/*test-name*"} - {:type "var" :ns "basilisp.test" :candidate "clojure.test/*test-section*"} - {:type "macro" :ns "basilisp.test" :candidate "clojure.test/are"} - {:type "macro" :ns "basilisp.test" :candidate "clojure.test/deftest"} - {:type "var" :ns "basilisp.test" :candidate "clojure.test/gen-assert"} - {:type "macro" :ns "basilisp.test" :candidate "clojure.test/is"} - {:type "macro" :ns "basilisp.test" :candidate "clojure.test/testing"} - {:type "var" :ns "basilisp.test" :candidate "clojure.test/use-fixtures"}]} - (client-recv! client))) + (let [result (client-recv! client) + has-completion? (set (:completions result))] + (is (= @id* (:id result))) + (is (= ["done"] (:status result))) + (are [completion] (has-completion? completion) + {:type "var" :ns "basilisp.test" :candidate "clojure.test/*test-failures*"} + {:type "var" :ns "basilisp.test" :candidate "clojure.test/*test-name*"} + {:type "var" :ns "basilisp.test" :candidate "clojure.test/*test-section*"} + {:type "macro" :ns "basilisp.test" :candidate "clojure.test/are"} + {:type "macro" :ns "basilisp.test" :candidate "clojure.test/deftest"} + {:type "var" :ns "basilisp.test" :candidate "clojure.test/gen-assert"} + {:type "macro" :ns "basilisp.test" :candidate "clojure.test/is"} + {:type "macro" :ns "basilisp.test" :candidate "clojure.test/testing"} + {:type "var" :ns "basilisp.test" :candidate "clojure.test/use-fixtures"})) + (client-send! client {:id (id-inc!) :op "complete" :prefix "test/"}) - (is (= {:id @id* :status ["done"] - :completions [{:type "var" :ns "basilisp.test" :candidate "test/*test-failures*"} - {:type "var" :ns "basilisp.test" :candidate "test/*test-name*"} - {:type "var" :ns "basilisp.test" :candidate "test/*test-section*"} - {:type "macro" :ns "basilisp.test" :candidate "test/are"} - {:type "macro" :ns "basilisp.test" :candidate "test/deftest"} - {:type "var" :ns "basilisp.test" :candidate "test/gen-assert"} - {:type "macro" :ns "basilisp.test" :candidate "test/is"} - {:type "macro" :ns "basilisp.test" :candidate "test/testing"} - {:type "var" :ns "basilisp.test" :candidate "test/use-fixtures"}]} - (client-recv! client)))))))) + (let [result (client-recv! client) + has-completion? (set (:completions result))] + (is (= @id* (:id result))) + (is (= ["done"] (:status result))) + (are [completion] (has-completion? completion) + {:type "var" :ns "basilisp.test" :candidate "test/*test-failures*"} + {:type "var" :ns "basilisp.test" :candidate "test/*test-name*"} + {:type "var" :ns "basilisp.test" :candidate "test/*test-section*"} + {:type "macro" :ns "basilisp.test" :candidate "test/are"} + {:type "macro" :ns "basilisp.test" :candidate "test/deftest"} + {:type "var" :ns "basilisp.test" :candidate "test/gen-assert"} + {:type "macro" :ns "basilisp.test" :candidate "test/is"} + {:type "macro" :ns "basilisp.test" :candidate "test/testing"} + {:type "var" :ns "basilisp.test" :candidate "test/use-fixtures"}))))))) (deftest nrepl-server-eval (testing "basic" diff --git a/tests/basilisp/test_test.lpy b/tests/basilisp/test_test.lpy new file mode 100644 index 00000000..c37521aa --- /dev/null +++ b/tests/basilisp/test_test.lpy @@ -0,0 +1,79 @@ +(ns tests.basilisp.test-test + (:require [basilisp.test :refer :all])) + +(defn- before-after-fixture + [events] + (fn [] + (swap! events conj :before) + (yield) + (swap! events conj :after))) + +(defn- index-fixture + [events idx] + (fn [] + (swap! events conj idx) + (yield) + (swap! events conj idx))) + +(def ^:dynamic *state* nil) + +(deftest with-fixtures-test + (testing "setup and teardown" + (let [events (atom [])] + (with-fixtures [(before-after-fixture events)] + (swap! events conj :during)) + (is (= [:before :during :after] @events)))) + + (testing "teardown on exception" + (let [events (atom [])] + (try + (with-fixtures [(before-after-fixture events)] + (swap! events conj :during) + (throw (ex-info "Boom!" {}))) + (catch Exception _ nil)) + (is (= [:before :during :after] @events)))) + + (testing "teardown on fixture setup exception" + (let [events (atom [])] + (try + (with-fixtures [(before-after-fixture events) + #(throw (ex-info "Boom!" {}))] + (swap! events conj :during)) + (catch Exception _ nil)) + (is (= [:before :after] @events)))) + + (testing "teardown on fixture teardown exception" + (let [events (atom [])] + (try + (with-fixtures [(before-after-fixture events) + (fn [] + (yield) + (throw (ex-info "Boom!" {})))] + (swap! events conj :during)) + (catch Exception _ nil)) + (is (= [:before :during :after] @events)))) + + (testing "applied in order" + (let [events (atom nil)] + (with-fixtures [(index-fixture events 1) + (index-fixture events 2) + (index-fixture events 3)] + (swap! events conj 4)) + (is (= '(1 2 3 4 3 2 1) @events)))) + + (testing "nesting fixtures" + (with-fixtures [(fn [] + (with-fixtures [(fn [] + (binding [*state* 1] + (yield)))] + (yield)))] + (is (= 1 *state*))))) + +(deftest join-fixtures-test + (testing "applied in order" + (let [events (atom nil)] + (with-fixtures [(join-fixtures [(index-fixture events 1) + (index-fixture events 2) + (index-fixture events 3)])] + (swap! events conj 4)) + (is (= '(1 2 3 4 3 2 1) @events)))))