Skip to content

Describing one checkable's prerequisites

marick edited this page Feb 27, 2013 · 10 revisions

Executable examples

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 know we'll eventually have to read the project file, but let's defer worrying about that. Instead, we simply declare there'll someday be a function named 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)

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 checkable; 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__checkable_specific.clj:15)
    Expected: ["test1" "test2" "source1"]
      Actual: nil
false

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

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

The combination of checkable and prerequisite made two checkable claims, neither of which succeed:

  • 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))))

The fact now succeeds.

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 checkables 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__checkable_specific.clj:20)
    Expected: ["test"]
      Actual: java.lang.Error: boom!
              as_documentation...(prerequisites__checkable_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 one 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 help you avoid such questions, Midje provides metaconstants. A metaconstant explicitly has no properties except identity and whatever properties you assign it via 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 list of courses:

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

Simple enough. However, the University administration has a new requirement for GPA calculation: the child of a wealthy alumnus gets an automatic half point increase in GPA.

gpa will need to take an argument describing the student. However, we don't want to assume too much about the data structure. In fact, we want to assume nothing more than that it provides the information gpa needs.

Here, then, is a fact that both checks the requirement and ensures our code won't be overly coupled to the student data structure:

(fact
  (let [correct-gpa 3.66
        tolerance 0.01
        coursework [{:credit-hours 1, :grade 5}
                    {:credit-hours 2, :grade 3}]]

    (gpa ..student.. coursework) => (roughly correct-gpa tolerance)
    (provided (child-of-wealthy-alumnus? ..student..) => false)
    
    (gpa ..student.. coursework) => (roughly (+ correct-gpa 0.5) tolerance)
    (provided (child-of-wealthy-alumnus? ..student..) => true)))

Exercises:

  1. Unlikely as it may seem to you, class warrior that you are, a legacy admission might have the work ethic to get good marks, ones leading to, say, a GPA of 4.7. Our thumb-on-the-scale computation would produce a GPA of 5.2, which is higher than the maximum possible. Update the fact to make 5.0 the maximum result of the gpa function. Improve the existing implementation.

  2. You might have found updating the fact somewhat annoying or tedious. There's an old saying: if the testing is hard, the problem is probably in your design. In this case, I think it's a problem that gpa smooshes two responsibilities together: that of making a fair GPA calculation, and that of cheating in some cases. Split the existing fact into two parts: one that describes fair calculations done by fair-gpa, and one that describes how gpa uses fair-gpa. The second fact should use a ..coursework.. metaconstant.

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, all is well.

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