Fork me on GitHub
#testing
<
2021-04-15
>
Proctor13:04:48

@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…

Noah Bogart13:04:48

depends on the purpose of the helper functions!

Proctor13:04:14

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'))))

Proctor13:04:15

it sometimes feels a bit imperative with all the let bindings from some of the other functional oriented languages I have done too

Noah Bogart13:04:15

from that example, it looks like you're doing the same thing twice, right?

Proctor13:04:37

various kind of building the input args

Proctor13:04:07

and it is likely a existing codebase thing in some cases and not a Clojure thing

Proctor13:04:42

I think it is more of a imperative use of let bindings more than anything

Noah Bogart13:04:52

i think that's okay

Noah Bogart13:04:06

if you have that kind of set of changes, a threading macro can make it look "nicer"

Noah Bogart13:04:19

if you're willing, real code is more helpful

Proctor13:04:32

but being new, it feels like that the let bindings make it harder to eval a form in a REPL

Noah Bogart13:04:06

writing a helper function that performs a set of transformations for you can be helpful, which is what I think you were suggesting above

Noah Bogart13:04:16

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

craftybones13:04:35

@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

👍 3
Proctor13:04:00

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

craftybones13:04:38

are is quite handy in certain cases where your output requires some transformation in order to compare, especially if your output is a map

craftybones13:04:41

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....

👍 3
craftybones13:04:20

of course, you could also use select-keys and is

Proctor14:04:20

dummy’ed out structure of a test I did that feels off…

Proctor14:04:22

(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)))
      )
    ))

Proctor14:04:39

And I can see how moving the result-case expressions and expected-form in the test/is would help

craftybones14:04:38

I like hard coding the expected values as an argument to is

👍 3
craftybones14:04:18

I like hard coding arguments too 🙂

Proctor14:04:21

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

craftybones14:04:30

That way, failures show what happened

Proctor14:04:30

and maybe it is a test and “eval form” don’t play as nice I hope;

craftybones14:04:58

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

craftybones14:04:58

(deftest some-test
  (testing "Some case")
    (is (= (actual-fn arg-1 arg-2...) expected)))))

craftybones14:04:26

I do use a let block occasionally, but try to avoid anything that isn’t contextual

Proctor14:04:45

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… 😄

Proctor14:04:36

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…

Proctor14:04:44

day job in Clojure changes all that…

craftybones14:04:09

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

craftybones14:04:20

My biggest learning with tests is to be as explicit as possible

Proctor14:04:10

yeah, Ruby and JS most recently, but even .NET before those, I tend to prefer any setup as local to the test as possible

Proctor14:04:52

and maybe some of it should be let bindings should prefer to be in a testing instead of at the top level of deftest

Noah Bogart14:04:13

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])))

Noah Bogart14:04:25

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

seancorfield14:04:13

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?

seancorfield14:04:56

(I find REPL-Driven Development and Test-Driven Development tend to produce code that is simpler and easier to test)

twashing15:04:41

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.

seancorfield15:04:35

The arguments are different between those two macros @twashing

twashing15:04:50

Correct. I need to invoke it differently. Does that affect the test system? deftest2 [name & body] vs defspec-test [name sym-or-syms]

seancorfield15:04:28

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.

3
twashing16:04:57

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)))))

twashing16:04:10

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))))

seancorfield16:04:16

Sorry, I don’t do any ClojureScript — only Clojure — and this looks specific to cljs.

Noah Bogart16:04:58

woah really? do you do selmer/templating for all front end stuff?

seancorfield16:04:04

@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).

👍 3
seancorfield16:04:34

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.

Noah Bogart17:04:31

That makes total sense. Thanks for the reply!