@nbtheduke I like that idea, and was thinking about that in some form… when you do that do you create a bunch of helper functions in the test namespace that help build that arg context? Do you do a bunch of “builder” functions (in the TDD world terminology) to keep the assertions simple…
depends on the purpose of the helper functions!
some of the things I have seen is the building of up the args over a couple of lines of the let
bindings as well…
(let [a ...
b ... a ...
c .... a .. b...
b' ... a ...
c' .... a .. b'...]
(is (= (form c) (form c'))))
it sometimes feels a bit imperative with all the let
bindings from some of the other functional oriented languages I have done too
from that example, it looks like you're doing the same thing twice, right?
various kind of building the input args
and it is likely a existing codebase thing in some cases and not a Clojure thing
I think it is more of a imperative use of let
bindings more than anything
i think that's okay
if you have that kind of set of changes, a threading macro can make it look "nicer"
if you're willing, real code is more helpful
but being new, it feels like that the let
bindings make it harder to eval a form in a REPL
yeah, it can
writing a helper function that performs a set of transformations for you can be helpful, which is what I think you were suggesting above
my codebase has a lot of legacy cruft from years of non-clojure devs working on it, so there are 3 "test helper" files that have various functions that perform set-up and tear down and manage that stuff, and slowly massaging it all into pure functions has helped a lot
@steven.proctor - in your let forms, are b and c and c’ just transforms of the result you wish to check? Have you used are
? That can potentially mitigate a few problems too
i saw are
, and that looked like it might solve some needs, I have done “ad-hoc” things like are
by eaching over things and running tests in a block in Ruby and JS before
are
is quite handy in certain cases where your output requires some transformation in order to compare, especially if your output is a map
For instance, you return a response with some optional headers that you aren’t interested in checking. There, writing an expected object that contains all the optional headers might be tedious and using are
would help it like so
(let [res (handler req)]
(t/are [x y] (= y (res x))
:status 200
:content-type....
of course, you could also use select-keys
and is
dummy’ed out structure of a test I did that feels off…
(test/deftest feels-hard-to-eval-in-repl-tests
(let [common-args {...}
args-case1 {...}
args-case2 {...}
error-args-case {...}
result-case1 (fn-under-test args-case1 common-args)
result-case2 (fn-under-test args-case2 common-args)
expected-form1 [:expected :things :here]
expected-form2 [:other :things :here]]
(test/testing "testing case 1"
(test/is (= expected-form1 result-case1)))
(test/testing "testing case 2"
(test/is (= expected-form2 result-case2)))
(test/testing "checking better error messages"
(test/is (thrown-with-msg? ExceptionInfo
"A nice error message"
(fn-under-test error-args-case common-args)))
)
))
And I can see how moving the result-case
expressions and expected-form
in the test/is
would help
I like hard coding the expected values as an argument to is
I like hard coding arguments too 🙂
but if trying to eval some of the (fn-under-test args-case1 common-args)
in the REPL to get details on what it is doing still seems a bit tricky
That way, failures show what happened
and maybe it is a test and “eval form” don’t play as nice I hope;
One place where the general DRY principle hurts is in tests. There is a tendency to pull out stuff like common args, but I like leaving them in there, even if they look bulky
(deftest some-test
(testing "Some case")
(is (= (actual-fn arg-1 arg-2...) expected)))))
I do use a let block occasionally, but try to avoid anything that isn’t contextual
Agree, in other languages I have found myself fighting the balance between needed setup and how much that setup distracts from what I am trying to test… 😄
any tests I have done before in Clojure has been all side/play stuff, so was’t too concerned about how will this be maintained…
day job in Clojure changes all that…
😉
Sure. I think it is easiest to go from raw values to fns later. If you abstract early, then it gets harder to maintain later
My biggest learning with tests is to be as explicit as possible
yeah, Ruby and JS most recently, but even .NET before those, I tend to prefer any setup as local to the test as possible
and maybe some of it should be let
bindings should prefer to be in a testing
instead of at the top level of deftest
test helper functions that i've written look like this:
(defn get-ice
"Get installed ice protecting server by position. If no pos, get all ice on the server."
([state server]
(get-in @state [:corp :servers server :ices]))
([state server pos]
(get-in @state [:corp :servers server :ices pos])))
not super meaningful by itself i now realize, but my intention is to show that a given helper function shouldn't be doing any business logic, just cutting out some of the typing so that you can more accurately demonstrate the desired function/effect
I think that if you’re finding yourself writing tests with that much setup, maybe you need to refactor your functions to be easier to test?
(I find REPL-Driven Development and Test-Driven Development tend to produce code that is simpler and easier to test)
I’m trying to wire into the cljs.test
reporting system with a custom macro. I’m following the pattern in cljs.test/deftest
:
https://cljs.github.io/api/cljs.test/deftest
https://github.com/clojure/clojurescript/blob/r1.10.773-2-g946348da/src/main/cljs/cljs/test.cljc#L230-L246
Copying and using deftest works just fine. But if I simply create my own test macro defspec-test
, and return the results, I get the error Cannot read property 'test' of undefined
. Anyone know what’s going on here?
util.cljc
(defmacro deftest2 [name & body]
(when cljs.analyzer/*load-tests*
`(do
(def ~(vary-meta name assoc :test `(fn [] ~@body))
(fn [] (cljs.test/test-var (.-cljs$lang$var ~name))))
(set! (.-cljs$lang$var ~name) (var ~name)))))
(defmacro defspec-test [name sym-or-syms]
(when cljs.analyzer/*load-tests*
`(do
(def ~(vary-meta name assoc :test `(fn [] ~sym-or-syms))
(fn [] (cljs.test/test-var (.-cljs$lang$var ~name))))
(set! (.-cljs$lang$var ~name) (var ~name)))))
mytest.cljs
(deftest2 zoobar
(t/is (= 1 1)))
(defspec-test coocoobar
(t/is (= 1 1)))
Run results
Testing mytest
ERROR in (coocoobar) (TypeError:NaN:NaN)
Uncaught exception, not in assertion.
expected: nil
actual: #object[TypeError TypeError: Cannot read property 'test' of undefined]
Ran 2 tests containing 2 assertions.
0 failures, 1 errors.
The arguments are different between those two macros @twashing
Correct. I need to invoke it differently. Does that affect the test system?
deftest2 [name & body]
vs
defspec-test [name sym-or-syms]
Ah, I see you’re expanding the argument(s) differently too. I would suggest looking at both expansions using macroexpand
— but I’m not sure how you’ll do that interactively with ClojureScript.
I can try the exact same implementation as in cljs/test.cljc
, but still get the error.
https://github.com/clojure/clojurescript/blob/r1.10.773-2-g946348da/src/main/cljs/cljs/test.cljc#L230-L246
(defmacro defspec-test [name & sym-or-syms]
(when cljs.analyzer/*load-tests*
`(do
(def ~(vary-meta name assoc :test `(fn [] ~@sym-or-syms))
(fn [] (cljs.test/test-var (.-cljs$lang$var ~name))))
(set! (.-cljs$lang$var ~name) (var ~name)))))
As per your Q, in a comment block you can do this.
(macroexpand
'(defspec-test coocoobar
(t/is (= 1 1))))
=> (def coocoobar (clojure.core/fn [] (clojure.test/test-var (var coocoobar))))
Sorry, I don’t do any ClojureScript — only Clojure — and this looks specific to cljs.
woah really? do you do selmer/templating for all front end stuff?
@seancorfield No problems!
@nbtheduke Our main app — the dating app on 40+ sites — is React.js, not cljs. We have a couple of Clojure apps that do SSR with Selmer (our login server, our billing server).
We explored cljs about seven years ago and decided it wasn’t ready for anything customer-facing at the time — the tooling was pretty bad and the language had a lot more differences from Clojure. When we started to build the new dating app (the old one was ColdFusion-based!), JS/React was really the only sane option for us. If we were starting over, maybe we’d use cljs. We are going to build a few new small apps where we are probably going to use cljs but the main app will stay JS for the foreseeable future.
That makes total sense. Thanks for the reply!