-
Notifications
You must be signed in to change notification settings - Fork 129
Metaconstants
(A video that uses metaconstants)
When designing, one of the things you have to decide is what to put off until later. The exact "shape" of the data is often a good thing to put off. Clojure helps you with that. You can decide you need to access elements of a collection in first-to-last order without having to settle on whether that collection is a list, a vector, or a lazy sequence. You can decide to write (:key thing)
without deciding whether the thing
is a map or a record.
You can also make up your own functions that have to work with your as-yet-undefined data structure. Below, I'll show an example of movie data. Instead of saying what a "movie" is, I'll just say it's something that works with the functions has-favorite-actor?
and critics-rating
. I'll worry later about what organization of data makes those functions easy to write and run fast enough (accepting that the need for speed might force me to invent different functions).
We've known about this style of design for a zillion years. Why bring it up now? It's because things break down when it comes to tests. Tests require real data---constant, completely defined values for the code to operate on. That can force you to make decisions earlier than you might like. And, especially as the data gets complicated (maps of sequences of maps...), tests take longer to write and update.
Metaconstants help with that problem. They let you say to the test "this symbol here---it stands in for the constant data you'd otherwise expect. Instead of calling actual functions on that data, I'll tell you what those functions would have returned if they'd been called for real. Use those values."
As an example, suppose I were working on a program to decide for me what movie to watch this weekend. Some of my program's rating should depend on those of my favorite critics, but I also want to bump the rating up a bit if the movie has one of my favorite actors in it. Here's a test:
(fact
(my-rating ...movie...) => (roughly (* 3 1.2))
(provided
(critics-rating ...movie...) => 3
(has-favorite-actor? ...movie...) => true))
Here, ...movie...
is a metaconstant. The only way the test restricts the real data is to declare that critics-rating
and has-favorite-actor?
must apply to it.
Here's an implementation that passes the test:
(defn has-favorite-actor? [movie])
(defn critics-rating [movie])
(defn my-rating [movie]
(if (has-favorite-actor? movie)
(* (critics-rating movie) 1.2)
(critics-rating movie)))
You can see that the metaconstants and the implementation's variables are at the same level of abstraction.
As of Midje 1.3-alpha2, metaconstants can also be surrounded with dashes as in --metaconstant--
. Although I prefer the dot notation, dashes allow metaconstants to stand in for functions:
(fact
(function-under-test --f-- [1 2 3]) => 8
(provided
(--f-- 1 2 3) => 8))
If --f--
were named ..f..
, the Clojure reader would interpret the last line above as an attempted method call:
You type: (..f.. 1 2 3) => 8))
You get: (. .f.. 1 2 3) => 8))
Experience has shown that it's easy to type ..mc...
when you meant to type ...mc...
. As of Midje 1.3-alpha5, the two are resolved to the same metaconstant. The same is true of --mc---
and ---mc---
. It is not the case, however, that ..mc..
is the same metaconstant as --mc--
.
-
In the above example, there's no need for
critics-rating
andhas-favorite-actor?
to be implemented before you check the fact. You can see more about that style in top-down testing. -
When writing such tests, you could use anything as a standin---in Ruby, I use strings---but I like the way the metaconstants stand out. Their only special property is that Midje will declare them for you when it sees them in a fact. So there's no need for this:
(def ...movie... "...movie...")
- Midje considers any symbol beginning and ending with a period as a metaconstant. So if three dots are too garish for you, use can use one or two.