Skip to content

A tutorial introduction

marick edited this page Feb 17, 2013 · 14 revisions

Add this to your .lein/profiles.clj file:

{:user {:plugins [[lein-midje "3.0-beta1"]]}}

Then find the temporary directory of your choice and type this:

% lein new midje quux

Lein-midje will produce a conventional directory structure containing sample test and source files:

Test and source directories

Midje lets you intermingle tests and source in the same file, but we'll stick with the traditional separation in this tutorial.

The project.clj file contains Midje itself as a development dependency:

(defproject quux "0.0.1-SNAPSHOT"
  :description "Cool new project to do things and stuff"
  :dependencies [[org.clojure/clojure "1.4.0"]]
  :profiles {:dev {:dependencies [[midje "1.5-beta1"]]}})  ;; <<==

For historical reasons, the main namespace for Midje is oddly named:

(ns quux.t-core
  (:use midje.sweet)               ;; <<==
  (:require [quux.core :as core]))

Facts

Midje's core construct is a fact. In an idealistic sense, a fact represents a claim about some world, typically the world of a program. Here's a claim about Clojure's implementation of arithmetic (and, as it happens, any correct implementation of arithmetic):

(fact "There exist numbers such that (+ n n) is equal to (* n n).")

Midje can't actually do anything with that claim, since (1) it can't read English, and (2) even if it could, it has no mechanism for searching through numbers until it finds a confirming example. Instead, you have to provide the example. Here are three of them. Two are good (true) examples, and one is bad (false):

(fact "There exist numbers such that (+ n n) is equal to (* n n)."
  (+ 0 0) => (* 0 0)
  (+ 2 2) => (* 2 2)
  (+ 4 4) => (* 4 4))

Read those as "zero plus zero is zero times zero", etc. Midje's syntax follows that used in most books that provide examples of what code does: there's the code in question, then some delimiter (most often some kind of arrow or a newline), then what you'd expect to see if you typed that code to a repl.

Midje uses the term prediction for such arrow-containing syntactic forms. I don't use "example", because in real life, good examples might contain more than one arrow form. I like "prediction" because it implies that, at some point, the prediction is either going to come true or not come true. For Midje predictions, that point is when Midje is run.

Let's use lein-midje to check the three predictions:

A lein-midje failure

(I'll be using screen shots to show how Midje uses terminal colors. You can turn colorization off if you like.)

The third prediction failed. We'll fix that, but let me first show you how to avoid the slow startup due to having to load the JVM, Clojure, and Midje for each lein midje.

Autotest

Many testing tools provide autotest. An autotest tool starts with one test run that checks everything. But instead of exiting, the tool then watches the source and test directories for changes. When it sees one, it reloads both the changed file and any files that depend on it, either directly or indirectly. (In Midje's case, the dependencies are those documented in each file's ns expression.)

Lein-midje can start up autotesting from the command line:

690 $ lein midje :autotest

However, I usually find it more useful to use the repl tools.

Autotesting in the repl

Do this:

Using the repl tools

I'll let you explore Midje's in-repl documentation on your own. For now, type the following:

Autotest in the repl

Notice that you still have the repl prompt, so you can combine autotesting with interactive repl development. For example, you can double-check results:

Using the repl tools

If you now delete the incorrect claim from t_core.clj, you'll see this when you save the file:

Using the repl tools

Extended equality and checkers

Midje doesn't use = to compare the left-hand to right-hand sides, but rather its own extended equality. There are various small – and two large – differences between = and extended equality.

The first of the large differences is the handling of regular expressions. Equality is a pretty useless concept for them, since regular expressions aren't even equal to non-identical copies:

user=> (= #"a.b" #"a.b")
false

In a Midje prediction, a regular expression on the right-hand side causes a check for a partial match:

user=> (fact "O wad    some pow'r" => #"wad\s+some")
true

That is, the checking is done with re-find.

The larger of the two large differences is the handling of functions on the right-hand side. Under extended equality, the actual result isn't compared to the function. Instead it is passed to the function and the result is checked for "truthiness" (that is, any value other than nil or false). Here's a simple example:

user=> (fact (+ 1 2) => even?)

FAIL at (NO_SOURCE_PATH:1)
Actual result did not agree with the checking function.
        Actual result: 3
    Checking function: even?
false

Midje comes prepackaged with useful functions, called checkers, for your predictions. The two most used are probably truthy and falsey. Consider this expression:

(some even? [1 5])

The result of that expression is nil, so we could write this fact:

(fact
  (some even? [1 5]) => nil)

That works, but it does a poor job of expressing intent. Most often, we don't care specifically that the result is nil — we care that the result counts as false. So this is better:

(fact
  (some even? [1 5]) => falsey)

Note: you can also negate the arrow to force a check for inequality:

(fact
  (some even? [1 5]) =not=> truthy)

One disadvantage of truthy and falsey is that predictions using them often do not read clearly because they don't match the way we normally speak or write. Consider [1 3 5 8]. Most people would be more likely to say "The array has some even element" than "A search for some even element would yield a truthy value." We can use extended equality to match the way people speak:

(fact
  [1 3 5 8] => #(some even? %)   ; or...
  [1 3 5 8] => (partial some even?))

Unless you're a huge fan of higher-order functions and can grasp their meaning immediately, that looks kind of awkward. Midje provides a checker that looks nicer:

(fact
  [1 3 5 8] => (contains even?))

This works because contains interprets its arguments using extended equality (so that functions are given actual results as arguments). However, it and other collection checkers are probably most often used with non-function values. Here are two examples. I've made them fail so you can see what failure output looks like:

user=> (fact {:a 1, :b 2} => (just {:a 1, :CCC 333}))

FAIL at (NO_SOURCE_PATH:1)
Actual result did not agree with the checking function.
        Actual result: {:a 1, :b 2}
    Checking function: (just {:CCC 333, :a 1})
    The checker said this about the reason:
        Best match found: {:a 1}
false
user=> (fact [1 2 3 4 5] => (contains [1 2 4]))

FAIL at (NO_SOURCE_PATH:1)
Actual result did not agree with the checking function.
        Actual result: [1 2 3 4 5]
    Checking function: (contains [1 2 4])
    The checker said this about the reason:
        Best match found: [1 2]
false

The second example fails because contains by default looks for a contiguous subsequence. That can be overridden:

user=> (fact [1 2 3 4 5] => (contains [1 2 4] :gaps-ok))
true

Top-down development and the logical structure of programs

Test-driven design can be done either bottom up (in a way reminiscent of traditional Lisp repl-driven development) or top down (as described in Growing Object-Oriented Software, Guided by Tests, one of the strong early inspirations for Midje). Since there are various paths through this user documentation, I'll point you to this introduction if you're interested in learning about how Midje views the top-down approach in a functional language.

In summary

With Midje, I've aimed to support bottom-up design, top-down design, and (most importantly) a smooth alternation between the two. I've also aimed to combine the ease of repl-based development with the long-term value of putting tests in files. To judge how well I've met my goals, you'll have to put Midje to use.

Clone this wiki locally