Skip to content

Thoughts about replacing background

marick edited this page May 12, 2012 · 7 revisions

background and against-background are used for two things: letting you establish default prerequisites that apply to many examples (arrow-forms, checks) or even facts, and doing setup and teardown around examples or facts. They've proven too hard to understand and awkward to implement. They need to be replaced.

One idea for replacing background is based on these ideas:

  1. Setup/teardown is a procedural idea, whereas Midje's dominant metaphor is one of facts, truth statements. A better way to think of setup is to think of telling the runtime: here's a set of facts that I want you to force to be true (whereas the prerequisites of provided are about simply assuming facts are true).

  2. Teardown is not a necessary idea. It's used to undo the state change done by setup. But suppose that you said setup was responsible for getting to the proper state (forcing the proper facts), no matter if the state is the pristine one or one contaminated by a previous test. How hard would that be? I'm suspecting not terribly. (This could leave global state, like a database, dirty - but how often is that actually a problem?)

  3. It's useful to think of the setup (or fact-forcing) process as being backward chaining. That is, to establish the prerequisites the fact wants, you might need to establish other prerequisites, which might need to establish others, ... As more than one person has observed, Midje has something of a logic programming flavor to it, which is as I intended, and this would extend that metaphor. (I could even imagine using something like core.logic as an execution engine, which raises enticing possibilities I won't get into.)

  4. Scoping in Midje (the equivalent of "context" in other tools) is done with nested facts. (Note: this proposal doesn't properly grapple with this.)

Assumed prerequisites

Here is how you assume prerequisites:

  (fact "something"
    (assume-prerequisites (ping 1) => 200          ; <<<===
                          (ping "1") => 200
                          (ping 'symbol) => 404)
    (f 'first) => 3     ; first example
    (f 'second) => 5    ; second example
    (provided 
      (ping 1) => 8))
  • The prerequisites are put into force before each example and checked after each one (so that :times arguments are obeyed). There is no way to "wrap" a prerequisite around multiple examples inside a fact and have its call count checked at the end of the wrapping scope. [I don't think that's particularly useful, given everything else, so I don't want to incur the cost in complexity.]

  • As is now the case, a provided prerequisite takes precedence over one created by assume-prerequisites. (As in the second example.)

There's also no way to wrap assumed prerequisites around multiple facts. That is, there's nothing like this:

  (with-assumed-prerequisites ...
    (fact ...)
    (fact ...)
    (fact ...))

Instead, take advantage of nested facts:

  (fact "big claim"
    (assume-prerequisite (outer 1) => 5)
    (fact "subclaim"
      (assume-prerequisite (inner 1) => 5)
      ;; examples here see both the inner and outer prerequisites
      )
    ;; An example here would see only the outer prerequisite.
    (fact "another subclaim")
      (assume-prerequisite (inner 1) => "55555555555555555555")
      ;; examples here see both prerequisites, but note that
      ;; `(inner 1)` returns a different value. 
      ))

It is also possible to define prerequisite groups as shorthand for multiple prerequisites. Since a prerequisite group is just saying "this is true, and that is true, and also the other is true", they themselves are titled with prerequisite notation:

  (prerequisite-group [(only-numberish-values-succeed?) => true]
    (ping 1) => 200
    (ping "1") => 200
    (ping 'symbol) => 404)

  (fact "text"
    (assume-prerequisite (only-numberish-values-succeed?) => true)
    ...)

Prerequisite groups are looked up by pattern matching (specifically, unification) with bindings made available to the body of the prerequisite group:

  (prerequisite-group [(numberish-value-status) => ?desired-status]
    (ping 1) => ?desired-status
    (ping "1") => ?desired-status
    (ping 'symbol) => 404)
  
  (fact "text"
    (assume-prerequisite (numberish-value-status) => 200)
    ...)

Per-example forced prerequisites

To force a prerequisite to become true, you use ensure-prerequisite:

(fact 
  (ensure-prerequisite (log-contents) => empty?)
  ...) 
  • The code associated with ensure-prerequisite is run before each example.

  • ensure-prerequisite takes effect when it's executed. That means you could do this:

       (fact
         (f 1) => 2  ; log in undefined state
         (ensure-prerequisite (log-contents) => empty?)
         (f 1) => 2)  ; log is forced to be empty
         ...)

    ... but I think it's probably too confusing.

  • There's a subtle difference in the right-hand-side of the arrow. If an assumed prerequisite had empty? on its right-hand-side, that'd mean that the result of calling log-contents would be the function empty?. Here, it should be interpreted as saying, "Make it such that the checker empty? would return true if given the value of (log-contents). I think this latter interpretation is more expressive.

    But it's really just a matter of convention. The "signature" (log-contents) => empty? is just used to look up the forcing function, and the implementation of that function can do whatever it wants.

A forcing function is defined like this:

  (to-ensure [(log-contents) => empty?]
    (reset! log []))

Forcing functions are also looked up by unification and provide bindings to the function body:

  (to-ensure [(log-contents) => (contains ?value)]
    (reset! log (range 1 (inc ?value))))

Since the body of the function definition is just code, it can arrange to do its job however it wants. However, I hope that people will do something logic-programming-like. Here's a simple example:

  (fact
    (ensure-prerequisite (horse-count) => 5
                         (cow-count) => 5)
    ...)

  ;; ... which depends on an earlier:

  (to-ensure [(horse-count) => ?number]
    (ensure-prerequisite (animal-count :species :horse) => ?number))

  ;; ... which depends on an earlier:

  (to-ensure [(animal-count :species ?species) => ?number]
    (dotimes [n ?number]
      (create-animal :species ?species
                     :name  (str (name ?species) "-" n))))

More interesting would be to use core.logic, but I don't know much about it yet.

Forced prerequisites that span examples

When testing a function that produces side-effects, you typically want more than one example of its behavior. The following checks both the function's return value and its side-effect.:

(fact "you can insert into the database"
  (insert :table, :greeting "hi", :person "mom!") => truthy
  (let [matches (select :table, :greeting "hi")
        match (first matches)]
    (count matches) => 1
    (:greeting match) => "hi"
    (:person match) => "mom!"))

If, say, an empty table were forced for each example, this test could not work. The check of the count would be followed by the emptying of the table, which would break the next check. Instead, the empty table needs to be forced once. That's done like this:

(fact "you can insert into the database"
  (ensure-prerequisite-here (table :table) => empty?)    ; <<<<<<< 
  (insert :table, :greeting "hi", :person "mom!") => truthy
  ...)

Since ensure-prerequisite-here can be called anywhere, if you absolutely need for the database to be cleared at the end of your tests, you can do this:

BEM: ICK!

  (fact (ensure-prerequisite-here (table :table) => empty?) ...)
  (fact (ensure-prerequisite-here (table :table) => empty?) ...)
  (fact (ensure-prerequisite-here (table :table) => empty?)) ...)
  (fact (ensure-prerequisite-here (table :table) => empty?) ...)
  (ensure-prerequisite-here (ensure-prerequisite-here (table :table) => empty?))

Problems

  1. Repeating for each fact that you want the table to be empty is lame. It'd be better to have an enclosing fact describe what's to happen at the beginning of each enclosed fact. Have ensure-prerequisite take effect before each "thing" (example or fact) that's encountered, but not again before things enclosed in that thing?

  2. To simply reset an atom requires that you both name it @log => empty? and write a to-ensure description. Couldn't there be shorthand?

Clone this wiki locally