Skip to content

A tutorial introduction for clojure.test users

marick edited this page Feb 12, 2013 · 47 revisions

This tutorial shows a clojure.test user how to migrate to Midje. That migration can be gradual: Midje coexists with clojure.test, so you can use both at the same time, even in the same file.

A sample project

If you want to follow along with this tutorial, you can fetch this project:

% git clone git@github.com:marick/midje-clojure-test-tutorial.git

The project is about a whimsical little function called migrate that "moves" key/value pairs from one map to another. Migrate calls are written left to right: (migrate source-map key-to-migrate destination-map), and the result is the "changed" [source, destination] pair after the migration:

user=> (migrate {:a 1} :a {})
[{} {:a 1}]

You can migrate more than one key:

user=> (migrate {:a 1, :b 2, :c 3} :b :c {})
[{:a 1} {:b 2, :c 3}]

In the case of key clashes, the migration isn't done:

user=> (migrate {:a 1, :b 2} :a :b {:a "not-1"})
[{:a 1} {:b 2, :a "not-1"}]

Tests and facts

migrate has tests. Here are the first two of them:

(deftest migration
  (testing "migration produces two maps with keys (and values) from one moved to the other"
    (is (= [{} {:a 1}]
           (migrate {:a 1} :a {}))))
  (testing "duplicates are not migrated"
    (is (= [{:a "not moved"} {:a "retained"}]
           (migrate {:a "not moved"} :a {:a "retained"})))))

To translate those tests into Midje, you first need to add it to your project.clj file:

  :dependencies ...             ;; typically, :dev dependencies
                [midje "1.5.0"]
                ... 

Then use midje.sweet in your test namespace:

  (:use clojure.test      ;; No harm in retaining this
        midje.sweet       ;; <<<<  
        migration.core)

(The name midje.sweet is a historical artifact: Midje used to have three distinct top-level namespaces.)

Now you can write Midje code that looks structurally very like the clojure.test tests, except for different jargon:

(facts migration
  (fact "migration produces two maps with keys (and values) from one moved to the other"
    (migrate {:a 1} :a {}) => [{} {:a 1}])
  (fact "duplicates are not migrated"
    (migrate {:a "not moved"} :a {:a "retained"}) => [{:a "not moved"} {:a "retained"}]))

The most obvious difference is that the is expressions have been replaced by ones modeled after the way everyone writes examples in documentation: source on the left, some sort of delimiter (like an arrow or newline or comment symbol or repl prompt) to separate the source from the result, and then the result.

Midje uses the term prediction for its arrow-containing syntactic forms. That's because writing such arrow forms is a prediction about what happens when you load the file containing them. ("If migrate is ever called with these arguments, its result will be such-qnd-so.") You expect the predictions to come true. If they don't, either the predictions were wrongly written or what you claimed was a fact isn't in fact a fact.

A lot of the structure in the previous example of a fact is optional. For example, here's a minimalist version:

(fact
  (migrate {:a 1} :a {}) => [{} {:a 1}]
  (migrate {:a "not moved"} :a {:a "retained"}) => [{:a "not moved"} {:a "retained"}])

Running tests

When you run the tests with lein tests, midje failures print out:

A midje failure message in lein test output

(I used a screen shot here to emphasize that Midje by default uses terminal colors in its output.)

Unfortunately, even though Midje found a failure, it's not included in the clojure.test's failure count. For that reason, I recommend you use Midje's own Leiningen plugin. To install it, add this to your :user profile in ${HOME}/.lein/profiles.clj:

{:plugins [... [lein-midje "3.0"] ...]}

Now you can do this:

Lein midje output

Notice that both Midje and clojure.test output are reported (and colored so that failures stand out). Both Midje's and clojure.test's failure counts are reported. They are also both used to construct the exit status, which is 0 if there were no problems, a non-zero number otherwise. (Strictly, the exit status is the number of failures, up to 255. So in this case, the exit status is 2.)

Because Clojure and Midje's startup time is slow, you will probably prefer to use "autotest", in which Midje watches your project for changed files. When it sees a change, it reloads the changed files and all files that depend on it (either directly or indirectly). In the following example, I start autotest on a buggy version of migrate, make (and save) a syntax error trying to fix it, and then fix it for real. (I've removed the clojure.test tests to keep the output from flooding the screen.)

Lein midje output

I personally prefer to start autotest from within the repl, using Midje's Repl Tools. That makes it easy to fluidly switch between test-driven development and repl-driven development:

Lein midje output

However, let's move on to more about how predictions work.

Checkers

core_test.clj includes a test that doesn't use =:

  (testing "a rather silly test"
    (is (even? (count (migrate {:a 1} :a {})))))

An equivalent fact looks like this:

(fact
  (count (migrate {:a 1} :a {})) => even?)

When a function appears on the right-hand side of the arrow, the result of the left-hand side is passed to that function. If the function returns a "truthy" value, the fact checks out. Midje comes with a set of predefined checkers.

Suppose we wanted to make a prediction about only the "destination" part of migrate's output. We could do this:

(fact
  (migrate {:a 1} :a {}) => (contains {:a 1}))

That's actually a weak prediction. It would be fooled by the case where the source, rather than the destination, part of the result is {:a 1}. Here's a more precise prediction that wouldn't be fooled, but still doesn't force you to "complect" together the creation of a result and choosing which pieces to focus on:

(fact
  (migrate {:a 1} :a {}) => (just [irrelevant {:a 1}]))

(just is a checker that insists on a match for every piece of the left-hand-side result. irrelevant matches anything.)

Two other common checkers are truthy and falsey. Consider this clojure.test expression:

(is (not (some even? [1 5])))

The corresponding fact would be this:

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

That works, but it does a poor job of expressing intent. We don't care specifically that the result is nil – we care that the result counts as false. A better way to translate the code would be with the falsey checker:

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

Extended equality

A Midje prediction like this:

(produce-actual-result) => expected-result

... is translated into this:

(extended-= (produce-actual-result) expected-result)

As you've seen, expected results that are functions are treated specially. The most important other case of extended equality is regular expressions. Consider this prediction:

(fact
  (str "the number is " 5) => #"number.*5")

That doesn't mean "the resulting string will be equal to a particular regular expression" (which it could never be). Instead, it means "succeed if there's a substring of the result that matches the regular expression". (To be specific: extended equality uses re-find.)

Tabular tests

Clojure.test has a way of writing tests in a tabular form:

(are [x] (= (+ x x) (* x x))
     0
     2)

In Midje, you'd write this:

(tabular
  (fact (+ ?x ?x) => (* ?x ?x))
  ?x
  0
  2)

Moving beyond what clojure.test can do

One way of coding is what Abelson and Sussman have called "programming by wishful thinking". You begin to write some tough function. Partway through, you see a piece of the problem that could nicely be solved by another function. Instead of writing it, you just assume that someone must have written that function before you. You'll solve your problem in terms of it, then go find it. When it turns out you can't find it, you write it. In the process, you might wishfully think that a third function must be available.

And so on – until all the functions you need actually have already been written.

An awkwardness with this kind of coding is that you can't actually run the original function until all the work has been done. For some of us, that's a long time to work without seeing concrete forward progress.

Midje supports this top-down style with prerequisites. A prerequisite documents the relationship between one function and another (which may not be written yet). As an example, suppose that your job is to read old-style LED numbers represented like this:

(fact
  (account-number ["    _  _     _  _  _  _  _ "
                   "  | _| _||_||_ |_   ||_||_|"
                   "  ||_  _|  | _||_|  ||_| _|"])
  => "123456789")

Call that input a "parcel". (I'm thinking of it as something like a two-dimensional parcel of land on a map.) You might reason that you can solve the problem by getting the digit for each character, then putting them all together into a string. That could look like this:

(unfinished digit-parcels digit)

(fact "an account number is constructed from character parcels"
  (account-number ..parcel..) => "01"
  (provided
    (digit-parcels ..parcel..) => [..0-parcel.. ..1-parcel..]
    (digit ..0-parcel..) => 0
    (digit ..1-parcel..) => 1))

There are a couple of interesting things here. First, the relationship between account-number and two helper functions is laid out before the helpers are defined. We've broken a larger problem into three smaller problems, one of which (how results are assembled) can be coded and checked immediately.

The other interesting thing is that we've abstracted away from a concrete data representation by using metaconstants. This fact documents that account-number doesn't actually depend on the "shape" of the data at all: it hands that issue off to digit-parcels. When coding in this style, you can often go quite a way before having to commit to specific data structures.

Stepping up a level of abstraction, I find that using prerequisites lets me (but does not require me to!) think of a program as a logical structure. It is a collection of functions annotated with checkable facts. Moreover, those functions are connected by "uses" relations documented with facts about how one function uses another:

Interrelationships

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 also aim to combine the ease of repl-based development with the long-term value you get by putting tests in files. To see how well I've met my goal, you'll have to read on and put Midje to use.

Anything else?

If you're a clojure.test user and you'd like this tutorial (or the whole user guide) to cover anything else, send mail to marick@exampler.com.

Clone this wiki locally