Skip to content

OLD: A tutorial introduction for clojure.test users

marick edited this page Feb 24, 2013 · 1 revision

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. (I've already done that for you.)

  :dependencies ...             ;; typically, in the :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) that separates 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-and-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 are printed. To show that, I've changed the expected result to make it wrong:

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, clojure.test's failure count doesn't include it. 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 file 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

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. Since clojure.core's predicates aren't all that useful for testing, Midje comes with a set of predefined checkers. For example, 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 creating the 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)

However, more idiomatic Midje would be this:

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

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

There are, roughly, two styles of test-driven design. One is bottom up, where you construct working simple functions and then write other functions that use them. (This is reminiscent of traditional Lisp repl-driven development). The other style is top down (best described in Growing Object-Oriented Software, Guided by Tests, one of the strong early inspirations for Midje).

Clojure.test supports the first. Midje supports both. 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.

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