A Clojure application generator with Midje
01 Feb 2016If 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.