Rodrigo Flores's Corner Code, Cats, Books, Coffee

A Clojure application generator with Midje

If you're bootstraping a new Clojure application, you would run this command:

lein new app my-awesome-app

And then Leiningen would generate a directory containing the bare minimum to make your application build (with a Hello World example). However, everytime I did that for experimenting purposes, I found that I've always added two libraries: Midje and Schema, so to help me stop doing this repetitive work, I created a template (aka generator) for that. Doing this, I can bootstrap an application with both libraries by just running this command:

lein new app-with-midje my-awesome-app

Midje

Midje is a testing framework for Clojure. I like the way tests are written in Midje:

(facts "average of items
  (fact "average of 1 item is itself"
    (average 1) => 1)
  (fact "average of 2 items is the minor item plus half the distance"
    (average 1 5) => 3)
  (tabular
    (fact "calculates average"
      (average ?a ?b ?c) => ?average)
    ?a ?b ?c ?average
    1  9  11 7
    3  12 30 15
    30 60 90 60))

They're very similar to what you would describe as input and output examples of a function. Also, I think tabular examples shows clearly what you would expect on each case, without having to duplicate code on each fact. It is also possible to mock and redef other functions on a test scope, so you can create isolated unit tests, and it is also possible to build your own checks, so you can create better tests based on your domain. So, in general, I consider Midje to be a great testing library to work with.

Schema

Clojure is not a typed language, so it is fairly common to see maps being used as a kind of typed data. Suppose you have this map describing a person:

(def person {:name "Joe Doe"
             :age  45
             :team "Blackburn Rovers"})

(def another-person {:name "Jane Doe"
                     :age  48})

For some reason, you want to give Joe and Jane a football jersey of their favourite football team. However, you forgot that this field may be not filled, as it is not mandatory for everyone to have a football team, so you wrote your function like this:

(defn ship-football-shirt [customer]
  (ship-shirt customer (buy-shirt (:team customer)))

As you forgot that it is a required, when you call the function with Joe, it works because he has a football team, but when you for Jane, it gives you a NullPointerException (Clojure normally raises a NPE you treat a nil like a map). How to get over this kind of issue ?

To help deal with this kind of problem, there is a library called Schema. After specifying it on your project.clj, you can instantiate on your namespaces and use this way

(require '[schema.core :as s])

(def teams #{"Blackburn Rovers" "Leicester United" ... } )
(def all-teams-ever (s/enum teams)) ; I'm not writing the name of all teams ever


(def Person {(s/required-key :name) s/Str
             (s/required-key :age)  s/Int
             :team                  all-teams-ever})

(s/validate Person person)

To declare a map schema, you should insert the keys and the accepted values for each key. Optionally, you can say that a key is required. If you call the s/validate function with a schema and a map, it will try to validate the type of each value and also the presence of all required keys, throwing an exception in case something doesn't validate. On our example, it requires a person to have a :name key with any string value, an :age key with any integer value and optionally a team with any value specified in the set teams.

To help fix our problem, we can create a derived schema call FootballFan:

(def FootballFan (assoc (dissoc Person :team) (s/required-key :team) all-teams-ever))

And then you can rewrite ship-football-shirt this way:

(s/defn ship-football-shirt [customer :- FootballFan]
  (ship-shirt customer (buy-shirt (:team customer)))

The :- symbol means that the customer symbol should be validated with that schema. When you call the function above inside the body of the macro (s/with-fn-validation ) it will trigger an exception. Why is this function validation optional ? Performance reasons mostly: makes sense to have this check turned on throughout your code on a testing environment but on production it might spend valuable time checking schemas (but a validate call on strategic places like before inserting something to a database or when you're receiving data from and HTTP request definitely makes sense).

Will it avoid receiving an exception ? Absolutely not, you will still get an exception, but this time you will receive a specific error telling you that Jane is missing that key/value. This kind of validation avoid not only strange errors, but you also don't need to program defensively inside ship-football-shirt (as you specified that you only accept maps that validate with that schema).

The App with Midje template

As I use both libraries on almost every application, I created a template for that: a set of files and folders generated given a project name. So, after adding it to my lein profile, I can call lein new app-with-midje awesome-project and it will generate a project.clj with both libraries, an APACHE V2 license,a README, a .gitignore file, a core.clj file to write the application (and a correspondent test file), a repl.clj file that serves as a REPL wrapper to load libraries and functions (like an the autotesting namespace) for general REPL there.

To use the generator, add on your ~/.lein/profiles.clj the plugin and the version (in case you already have the plugin vector, just append it): {:user {:plugins [[app-with-midje/lein-template "0.1.0"]]}}

And then, to generate the application:

lein new app-with-midje my-awesome-app

You can also check the code for the template on Github.

comments powered by Disqus