2
votes

I just finished my first six weeks working with Clojure and so far I'm pretty happy with the language. I'm developing my personal blog with leiningen and PostgreSQL. I already can publish new content, upload files and I have sessions, cookies and roles, anyway I think at this point I have enough code to start to worry about the testing section, but I'm kind of stuck since looks like a lot of things are happening in the clojure's testing and spec side.

So I have this function:

(defn download
  "GET /admin/uploads/download/:id"
 [params]
 (let [id       (-> params :id)
       upload   (model-upload/get-upload id)
       filename (:filename upload)
       body     (clojure.java.io/file (str "public/uploads/" filename))]
{:status 200
 :body body
 :headers {"Content-Type" "application/pdf"
           "Content-Length" (str (.length body))
           "Cache-Control" "no-cache"
           "Content-Disposition" (str "attachment; filename=" filename)}}))

The function takes a map as argument and delivers a final map to be sent and processed by compojure. I come from a Rails world so the way to test this function in Rails would be to create a FactoryGirl class, create a Rspec model file with the classic:

 expect(first_map).to eq(map_returned_by_function)

in it comparing what is expected, and then to run the rspec from the command line to get the green or red line.

Since yesterday I'm trying to replicate that process with Clojure using this doc:

https://www.codesai.com/2018/03/kata-generating-bingo-cards

but I think there is not yet a "standard" way to do a test including the DB (CRUD) part in Clojure. I don't even know where to put the spec files. I see Clojure libraries similar to FactoryGirl but I don't know if I should create my own data structures with spec so I'm not sure where to start, there are clojure.test.check.generators and spec generators but I don't know if they are different or if I should use only spec but not clojure.test.check. Can I run a test from the command line and not inside the REPL?

I mean: is there a document or tutorial about how to test a set of CRUD functions? I think I just need the initial HOWTO and then I could take it from there and I'll write a tutorial to newbies like me.

UPDATED:

It looks like Midje is what I'm looking for:

https://github.com/marick/Midje/wiki/A-tutorial-introduction

3

3 Answers

2
votes

It's idiomatic in Clojure to push IO to the edges of your application. Instead of reading from the DB inside your download function, you pass in the data read from the DB into your download function in the param map. Then you write your tests against the pure part.

Your function would end up looking like this:

(defn download-without-db
"GET /admin/uploads/download/:id"
 [params]
 (let [upload   (-> params :upload)
       filename (:filename upload)
       body     (clojure.java.io/file (str "public/uploads/" filename))]
{:status 200
 :body body
 :headers {"Content-Type" "application/pdf"
           "Content-Length" (str (.length body))
           "Cache-Control" "no-cache"
           "Content-Disposition" (str "attachment; filename=" filename)}}))

(defn get-upload-from-db [params]
    (assoc params :upload (-> params :id model-upload/get-upload)))

(defn download [params]
    (-> params 
        get-upload-from-db
        download-without-db))
1
votes

You're just looking for clojure.test. It even mentions relationships with RSpec in its doc.

This is included as part of Clojure itself, no dependencies are needed, and I'd recommend you get familiar with it first before using a non standard test framework like Midje, since it is the defacto test framework for Clojure, and the most popular one.

You would write a test as such:

(deftest download
  (testing "With valid input"
    (testing "it should return a header map with filename included"
      (is (= first_map (unit/download {:id 1}))))))

Now, Clojure is not object oriented, so there are no objects to mock. That said, you often use Java form within Clojure, and Java provides classes and objects. If you want to mock them easily, you can use the Java mocking framework called Mockito.

In your case though, the download function does not use any Java objects. So you don't need too.

Now, if you want this to be an integration test, the test I wrote is good enough for you. If you want this to be a unit test, and I assume (model-upload/get-upload id) does some IO, you'll want to mock the model-upload/get-upload function. You can easily do this using with-redefs-fn:

(deftest download
  (testing "With valid input"
    (testing "it should return a header map with filename included"
      (with-redefs-fn {#'model-upload/get-upload (constantly {:filename "cool.pdf"})}
        (is (= first_map (unit/download {:id 1})))))))

Or you can use with-redefs:

(deftest download
  (with-redefs [model-upload/get-upload (constantly {:filename "cool.pdf"})]
    (testing "With valid input"
      (testing "it should return a header map with filename included"
        (is (= first_map (unit/download {:id 1})))))))