Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Adventures in Clojure: TDD

Getting started with Clojure

Several months ago, I had picked up a copy of Seven Languages in Seven Weeks and I had got through about half of the Clojure chapter before I got distracted by something else. After a few conversations recently, Clojure popped onto my radar once again. I wanted a project where I could exercise a web stack, but also didn’t want to take on too much. I decided to try rebuilding my personal site, which currently is a static site which displays blog posts and speaking material.

I got warmed up by watching Stuart Sierra’s talk where he discusses the perils of global state. I walked through the tutorial on ClojureScript, Om and web frameworks like Luminus. I felt I needed to step down a layer and not be over reliant on non-core libraries.

You can see the code on Github, but for now I wanted to describe my initial steps into doing TDD.

Testing in Clojure

This application hits the database to retrieve meta data about blog posts from various sites I’ve posted to down the years. I started out trying to use speclj which seemed to get a good writeup on various sites, and although it’s more comparable to RSpec I was getting some obtuse errors which was blocking me from progressing. I went back to clojure.test where I had better results.

One fun thing is running tests in the REPL, no waiting for the JVM to spin up for me.

(in-ns 'robb1e.models.post-test)
(run-tests)

OK, let’s take a look at the whole thing first, then I’ll break it down.

(ns robb1e.models.post-test
  (:require 
            [robb1e.models.post :as post]))

(use 'clj-time.core)
(use 'clj-time.coerce)

(def db "postgresql://localhost:5432/robb1e_test")

(defn clean-database [f]
  (post/delete-all db)
  (f))

(use-fixtures :each clean-database)

(deftest can-insert-new-post
  (testing "Inserting new records, and retriving those"
    (is (= 0 (count (post/all db))))
    (post/insert db {:excerpt "excerpt" :publication "publication" :uri "uri" :title "Hello, world" :published_at (to-sql-date (now))})
    (is (= 1 (count (post/all db))))))

(deftest can-get-posts
  (testing "Retriving existing records"
    (post/insert db {:excerpt "excerpt" :publication "publication" :uri "uri" :title "Hello, world" :published_at (to-sql-date (now))})
    (is (= "Hello, world" ((first (post/all db)) :title)))))

First we set the namespace this code is in, and we load the clojure.test namespace and we alias our target namespace under test.

(ns robb1e.models.post-test
  (:require 
            [robb1e.models.post :as post]))

Here we load a library which wraps the Joda time library in Java. I’m getting lots of warnings here, so I’ll review it’s usage, but for the moment this is giving me methods such as (now) and (to-sql-time time).

(use 'clj-time.core)
(use 'clj-time.coerce)

I declare a def which is executed only once, this gives me the connection string for the PostgreSQL database I’m using.

(def db "postgresql://localhost:5432/robb1e_test")

This is a mechanism for doing before and after functions, whereby the test function is passed to the filter function and the filter function has to execute the test function in the order of any requirements before or after the test. (post/delete-all db) is a custom function that we’ll look at a little later.


(defn clean-database [f]
  (post/delete-all db)
  (f))

(use-fixtures :each clean-database)

Now we jump into the tests. These insert a record, and asset it’s saved and can be retrieved successfully.


(deftest can-insert-new-post
  (testing "Inserting new records, and retriving those"
    (is (= 0 (count (post/all db))))
    (post/insert db {:excerpt "excerpt" :publication "publication" :uri "uri" :title "Hello, world" :published_at (to-sql-date (now))})
    (is (= 1 (count (post/all db))))))

(deftest can-get-posts
  (testing "Retriving existing records"
    (post/insert db {:excerpt "excerpt" :publication "publication" :uri "uri" :title "Hello, world" :published_at (to-sql-date (now))})
    (is (= "Hello, world" ((first (post/all db)) :title)))))

Databasing

Great, got all that? Let’s take a look at the functions under test. Once again there’s the namespace declaration and an alias of the JDBC library called sql. I use clj-time again before jumping into the functions of insert, all and delete-all.

The connection string gets passed in from the test on each function call, and various methods in the clojure.java.jdbc namespace get called such as insert!, query, and execute!.

(ns robb1e.models.post
  (:require ))

(use 'clj-time.core)
(use 'clj-time.coerce)

(defn insert [db post]
  (sql/insert! db :posts (merge post {:created_at (to-sql-date (now)) :updated_at (to-sql-date (now))})))

(defn all [db]
  (into [] (sql/query db ["SELECT * FROM posts ORDER BY id DESC"])))

(defn delete-all [db]
  (sql/execute! db ["DELETE FROM posts"]))

You may be asking why I pass the db connection string around so much. If you look at the Heroku example, they encourage you to put this in a function in the same namespace as the database calls.

(def spec (or (System/getenv "DATABASE_URL")
  "postgresql://localhost:5432/shouter"))

Going back to Stuart Sierra’s warnings of polluting the namespace, along with wanting to move towards a design where dependencies around instead of having an implicit dependency, I’m exploring just that. By passing the connection string around, the tests are clean and could change fairly simply. In the actual application that configuration could be placed in an initializing block in one place rather then placed through the code base.

It’s something I’m exploring, but something I’ll hopefully be blogging more about.

Comments
  1. Sid Raval says:

    Very cool! I recently got started with Clojure, via ClojureScript. Just last night I was thinking, “I should start doing this TDD…” — your post will help!

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *