Inline tests, do you do it?

When my project allows it, I like to use the attrs-map of functions to inline my tests. Seeing that not even @danielcompton seems to be aware of this feature, I thought I might tip about it here.

The thing is that the Clojure test runner looks for :test in the attrs-map of defined functions and runs them. Here’s a super simple example:

(defn abs
  "The absolute value of a number."
   {:test (fn []
            (is (= (Math/abs 5) 5))
            (is (= (Math/abs -2) 2))
            (is (= (Math/abs 0) 0)))}
  [x]
  {:pre [(number? x)]}
  (if (pos? x)
    x
    (- x)))

I think this has lots of advantages. Keeping the (or at least, some) tests with the software under test makes for good documentation, promotes keeping the tests updated and is generally convenient.

With Clojure it gets extra powerful since you can use the REPL to grow the tests and the code you are testing without having to open some other file or window. I sometimes find myself entering a flow where I sort of mix REPL and test driven development. So instead of doing my experiments in the REPL window or even in a comment block I carry them out in that attrs-map, and then some of the experiments can just stay as tests.

It might not be for everyone, or for every occasion, but I recommend giving it a try. You might like it! :smile:

EDIT. In the original post I said this doesn’t work in ClojureScript. It does. See below. (I don’t know why it didn’t work when I tried this some while ago. It certainly wasn’t for lack of trying…)

7 Likes

It does?

FWIW I never do this and wouldn’t recommend doing it either.

When I write test I keep them separate since quite often the tests will become longer than the actual implementation, as your example already shows. Oftentimes the “tests” will require additional dependencies which would polute the original ns if done inline.

It doesn’t.

Yeah, the ratio of test code/tested code is often > 1. I don’t see it as problematic, though.

This certainly happens, and when it does I often move those tests out to a test namespace. It’s one of those reasons why ”(or at least, some) tests”. I also know of people who have written large and successful apps without worrying about the extra dependencies in the tested namespace.

[1:1]~cljs.user=> (require '[cljs.test :as t])
nil
[1:1]~cljs.user=> (defn foo {:test (fn [] (t/is (= 1 2)))} [])
#object[Function]
[1:1]~cljs.user=> (t/run-tests)

Testing cljs.user

FAIL in (foo) (at C:475:6)
expected: (= 1 2)
  actual: (not (= 1 2))

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
nil
1 Like

Wow, now I must go see why this hasn’t worked for me. There’s hope, thanks!

Hmm, in cljs/cljs/test.cljs I see this:

(defn test-var
  "If v has a function in its :test metadata, calls that function,
  add v to :testing-vars property of env."
  [v]
(run-block (test-var-block v)))

But I’m not sure how it’s hooked up. Have you put a note into https://dev.clojure.org/jira/?
(Or https://clojurescript.org/about/differences if it will remain a “difference from Clojure”?)

Wow, again. I can’t reproduce the problem I had. Super, duper nice! Will start inlining a lot of tests now.

Thanks. I have edited the post about this. It doesn’t seem to be a problem, as @thheller shows above. I am very confused now, but also happy.

You can also use https://clojuredocs.org/clojure.test/with-test

1 Like

@PEZ if you tested this on older versions of shadow-cljs it might indeed not have worked. I changed it a little while ago, so previously it did indeed not account for :test metadata.

1 Like

Phew! I was really beginning to wonder. :cowboy_hat_face:

I do something like this all the time! Not exactly inline tests, but I like keeping the test code in the same block when I’m developing.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Step 1
;;
;; I like to be able to C-c C-c on the block to see where
;; I'm at. So I evolve the function definition and the test
;; in sync.

(do
  (defn three []
    (+ 1 1))
  [(three)
   (= (three) 3)]
  )
;; => [2 false]

;; Not right -- work on it till it makes sense.

(do
  (defn three []
    (+ 1 1 1))
  [(three)
   (= (three) 3)]
  )
;; => [3 true]

;; there!

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Step 2a
;;
;; Small functions. Document the function interface with an
;; example in the docstring.

(defn three
  "Produce the number three

  (three)
  ;; => 3
  "
  []
  (+ 1 1 1))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Step 2b
;;
;; Larger functions. Make a proper test!

;; math.clj
(defn three
  "Produce the number three"
  []
  (+ 1 1 1))

;; math_test.clj
(deftest test-three
  (is (= 3 (three))))

2 Likes

As a counterpoint, I keep my tests with the code, usually in a deftest form right after the function. Main reasons:

  • They serve as documentation and a demonstration, showing behavior in corner cases (nils, empty collections, etc)
  • Tests which live in a separate file structure tend to get rusty over time

As to dependencies, since I only write unit tests, there are no dependencies.

I’m not saying this is the One True Way, I just wanted to point out that separating tests from code is not necessarily the One True Way either. There are many approaches to writing tests.

4 Likes

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.