Skip to content

Describing one checkable's prerequisites

marick edited this page Feb 25, 2013 · 10 revisions

Here is an example of using a prerequisite in the development of a function that combines the :test-paths and :source-paths keys in a Leiningen project file. We'll pretend there's already a function called read-project-file:

(unfinished read-project-file)

unfinished is similar to Clojure's declare in that it can take multiple arguments and define a var for each one of them. Unlike declare, it binds the var to a function that blows up if called:

user=> (read-project-file)
Error #'read-project-file has no implementation, but it was called like this:
(read-project-file )  midje.util.exceptions/user-error (exceptions.clj:13)

Such an error alerts you that the function under test uses the prerequisite but your fact doesn't mention it.

Let's begin with an empty fetch-project-paths and write a fact:

(defn fetch-project-paths []
  )

(fact "test and source paths are returned, tests first"
  (fetch-project-paths) => ["test1" "test2" "source1"]
  (provided
     (read-project-file) => {:test-paths ["test1" "test2"]
                             :source-paths ["source1"]}))

Note the somewhat peculiar syntax. The provided form follows the prediction; it isn't enclosed in it. A provided form can contain several prerequisites. Each of them describes a function call with arguments. When that function is called with matching arguments, it is made to return the right-hand side of the arrow.

Checking this fact will fail because it predicted a triplet but the actual value was nil:

FAIL "test and source paths are returned, tests first" at (prerequisites__prediction_specific.clj:15)
    Expected: ["test1" "test2" "source1"]
      Actual: nil
false

However, it also fails because the prerequisite function was never called:

FAIL at (prerequisites__prediction_specific.clj:17)
These calls were not made the right number of times:
    (read-project-file) [expected at least once, actually never called]

We've made two predictions, neither one of which came true:

  • fetch-project-paths will return a certain value.
  • In doing so, it will call read-project-file in a certain way.

We can now implement fetch-project-paths. Just for fun, we'll do it in a silly way:

(defn fetch-project-paths []
    (flatten ((juxt :test-paths :source-paths) (read-project-file))))

We're not done with the implementation, though. What if the project file doesn't exist? In that case, we want fetch-project-paths to return ["test"]. It might be better to do that in a separate fact, but I'm going to tack it on to the existing one to show how predictions with prerequisites stack up in a fact:

(fact "fetch-project-paths"
  (fetch-project-paths) => ["test1" "test2" "source1"]
  (provided
     (read-project-file) => {:test-paths ["test1" "test2"]
                             :source-paths ["source1"]})

  (fetch-project-paths) => ["test"]
  (provided
    (read-project-file) =throws=> (Error. "boom!")))

Notice that I've defined something new about the not-yet-implemented read-project-file: its behavior when no file exists. Throwing an Error may not be the best choice, but it lets me show the =throws=> arrow. See below for more about prerequisite arrows.

When this fact is checked, the result is this:

FAIL "fetch-project-paths" at (prerequisites__prediction_specific.clj:20)
    Expected: ["test"]
      Actual: java.lang.Error: boom!
              as_documentation...(prerequisites__prediction_specific.clj:14)

Now we can fix the code:

(defn fetch-project-paths []
  (try
    (flatten ((juxt :test-paths :source-paths) (read-project-file)))
  (catch Error ex
    ["test"])))

The fact will now check out. And here's a prettier version of the test:

(facts "about fetch-project-paths"
  (fact "returns the project file's test and source paths, in that order"
    (fetch-project-paths) => ["test1" "test2" "source1"]
    (provided
      (read-project-file) => {:test-paths ["test1" "test2"]
                              :source-paths ["source1"]}))
  (fact "returns [\\"test\\"] if there is no project file."
    (fetch-project-paths) => ["test"]
    (provided
      (read-project-file) =throws=> (Error. "boom!"))))

Metaconstants

Generally, I don't like constants like "test1" and the like in my facts. In the fact above, they're harmless, but sometimes when you read them, you wonder: "Is this just a randomly-chosen value, or is there something special about it? something that matters to what the function under test does?"

To avoid such questions, Midje has metaconstants. A metaconstant explicitly has no properties except identity and whatever properties you give to it in a prerequisite. Here's an alternative to the first of the two facts above:

(fact "fetch-project-paths returns the project file's test and source paths, in that order"
  (fetch-project-paths) => [..test1.. ..test2.. ..source..]
  (provided
    (read-project-file) => {:test-paths [..test1.. ..test2..]
                            :source-paths [..source..]}))

This leaves no doubt that the actual names have nothing to do with fetch-project-paths: all that can matter is the contents of two vectors.

To show how metaconstants work with prerequisites, consider a function that computes a grade point average from a student and a list of courses:

(fact "GPA is weighted by credit hours"
  (gpa ..student.. [{:credit-hours 1, :grade 5}
                    {:credit-hours 2, :grade 3}])
  => (roughly 3.66 0.01))

However, there's a catch. The child of a wealthy alumnus gets an automatic half point increase in GPA (up to a maximum of 5.0). Here's how that could be written:

(fact
  (let [three-six-six-coursework [{:credit-hours 1, :grade 5}
                                  {:credit-hours 2, :grade 3}]]

    (gpa ..student.. three-six-six-coursework) => (roughly 3.66 0.01)
    (provided (child-of-wealthy-alumnus? ..student..) => false)

    (gpa ..student.. three-six-six-coursework) => (roughly (+ 3.66 0.5) 0.01)
    (provided (child-of-wealthy-alumnus? ..student..) => true)))

Notice that we're committing to very little about whatever sort of data structure "student" might eventually be. All we know is that one predicate takes it as an argument.

Arguments

Prerequisites don't just say that a function was called. They say that it was called with particular arguments. If you don't care about the arguments, you can use the anything checker:

     (fact
       ...
       (provided
         (f anything) => 1))

Call counts

Prerequisites defined by provided must be called once. They may be called more than once. You can, however, adjust that. Here's how you insist that a prerequisite be called exactly two times:

   (provided
     (f 5) => 50 :times 2)

You can use sequences, including lazy sequences, to describe a range of times a prerequisite must be called.

   (provided
     (f 5) => 50 :times [2 3]
     (f 4) => 40 :times (range 3 33))

To say "this call is optional", use this idiom:

   (provided
     (f 5) => 50 :times (range))

Just for grins, you can also give a function:

   (provided
     (f 1) => 1 :times even?))

The function is given the actual count of how often the function was called. If it returns a truthy value, your prediction checks out.

The "not-called" case

Suppose you want to predict that a function will never be called. The syntax for that is awkward:

   (provided
    (f anything) => irrelevant :times 0))

Prerequisites use a variant of extended equality

Sometimes you might not know the exact value that will be given to a prerequisite. You can use any of the predefined checkers to match an argument (like they're used to match an actual result). This works, for example:

   (provided 
     (f (roughly 5.0 0.01)) => 89))

You can also use regular expressions:

   (provided
     (f #"Hello.world") => 3)

Argument matching isn't quite the same as extended equality, though. It differs in the handling of functions. Given how common higher-order functions are in Clojure, it's much more likely that a function as an argument is meant to be taken literally than to be used as a checker. Therefore, in the following:

(unfinished hilbertian)

(defn function-under-test [n]
  (hilbertian (if (pos? n) even? odd?)))

(fact 
  (function-under-test 3) => ..hilbertian-result..
  (provided
    (hilbertian even?) => ..hilbertian-result..))

... the prerequisite predicts that hilbertian will be called with the function even?, rather than with an even number. If you actually want a plain function to be used as an argument matcher, wrap it in as-checker:

  (provided
    (hilbertian (as-checker even?)) => ..hilbertian-result..))

Prerequisite function calls can be nested

Consider this code:

(defn function-under-test [n]
 (-> (first-est 1 n) second-est inc))

It's a fact that function-under-test depends on first-est and second-est. We can say more: it's a fact that it depends on a particular call to first-est. Also, the result of that call must be passed to second-est. Note that we don't actually care what the output of first-est is: the correctness of our function depends only on the composition of the two functions, not what the first of them actually does.

These relationships could be expressed like this:

(fact
  (function-under-test 5) => 101
  (provided
    (first-est 1 5) => ..some-result..
    (second-est ..some-result..) => 100))

However, if that intermediate value (..some-result..) really is irrelevant, it's annoying that we need to talk about it at all. Midje accommodates that by "unfolding" prerequisites. The previous fact is equivalent to this one:

(fact
  (function-under-test 5) => 101
  (provided
    (second-est (first-est 1 5)) => 100))

Midje actually constructs the two prerequisites of the previous version, generating the intermediate metaconstant for you.

Unfortunately, due to the care Midje has to take about expanding macros, you can't write the prerequisite in the threading form function-under-test uses.

(fact "This does NOT work"
  (function-under-test 5) => 101
  (provided
    (-> (first-est 1 5) second-est) => 100))

Things that will annoy you about prerequisites

  • Fixed argument arity

    Suppose you have a function with four arguments. All you care about is the first value. You wish you could write this:

    (provided
      (f 3 ...) => 8)

    You can't. You have to write this:

    (provided
      (f 3 anything anything anything) => 8)
  • not called (See "The not-called case" above.)

Clone this wiki locally