Factual background

marick edited this page Nov 3, 2010 · 4 revisions
Clone this wiki locally

Consider this set of facts:

 (facts
    (f 1) => truthy
    (provided (g 1) => true, (h 1) => true, (j 1) => true)
    (f 1) => falsey
    (provided (g 1) => false, (h 1) => true, (j 1) => true)
    (f 1) => falsey
    (provided (g 1) => true, (h 1) => false, (j 1) => true)
    (f 1) => truthy
    (provided (g 1) => true, (h 1) => true, (j 1) => false))

This is annoying to type, and it makes it hard to tell what's special about each case. I propose using a metaphor I saw in an old automated reasoning system: old-fashioned cartoons where the background stays fixed while the interesting things change in front of it (and, by doing so, "invalidate" part of the background). Like this:

(against-background
    (g 1) => true, (h 1) => true, (j 1) => true
    (facts
       (f 1) => truthy
       (f 1) => falsey (provided (g 1) => false)
       (f 1) => falsey (provided (h 1) => false)
       (f 1) => falsey (provided (j 1) => false)))

This still isn't wildly appealing, and there may be other notations later for saying "vary this one thing but leave everything else alone."

The same metaphor can be applied to state that has to be set up and torn down. Before each check, some facts about the world need to be established. After it, they may need to be un-established. So we can vary the previous notation, like this:

  (against-background
    (datastate :countries) =starts=> unadorned-empty-table
    (datastate :countries) =ends=> gone
    (facts ....))

or an abbreviated form:

 (against-background
    (datastate :countries) =starts=> unadorned-empty-table =ends=> gone

Behind the scenes, you'll have to provide implementatiions for make-unadorned-empty-table and make-empty. Is this making people do too much work for the sake of a consistent-ish notation and metaphor? Such functions will be passed the complete original form (unevaluated). It must yield a function that does whatever's appropriate. I plan to support a backward chaining style of setup, where the stateful fact is established by establishing other facts it depends on.

There's another complication: wrapper macros. Consider a database test where (for whatever reason), you wanted to recreate the database connection for each check. There's no way to do it without repeating yourself:

 (against-background
    (datastate :countries) =starts=> unadorned-empty-table =ends=> gone
    (facts
       (sql/with-connection db ... => ...)
       (sql/with-connection db ... => ...)
       (facts...))

Ugly. How about this?

(against-background
    (datastate :countries) =starts=> unadorned-empty-table =ends=> gone
    (sql/with-connection db ...each-check...)

    (facts...))

Of course, most of the time you wouldn't want to set up the connection multiple times. You can so indicate:

(against-background
    (datastate :countries) =starts=> unadorned-empty-table =ends=> gone
    (sql/with-connection db ...all-checks...)

    (facts...))

Details

  • If the function under test doesn't use one of the background prerequisites, that's OK. There'll be no "you said you depended on this but you don't actually" failure. Not sure if this is a good idea, but doing otherwise would probably require a lot of different nestings, which would be annoying.

  • I believe all the work can be done at compile / macroexpansion time, so this will also work for people who use deftest or equivalent functions.

  • against-background does no setup or teardown when the file is in production mode.

  • against-background can be nested.

  • =starts=> facts and entry into wrapper facts happen in the order they're declared. =ends=> facts and exit from wrapper facts happen in reverse order.

  • Wondering if we want a background function that applies to the whole file (rather than having to wrap the whole thing). If so: background can appear multiple times. Each time it appears it wipes out the effect of any previous one.