Skip to content

Checkers for collections and strings

marick edited this page Feb 10, 2012 · 13 revisions

Midje comes with some general-purpose checkers that apply to datatypes that "look like" collections. (For these purposes, a string looks like a collection.) They are just, contains, has-prefix, has-suffix, and has.

These checkers can be nested to form checks of tree-like structures. That can lead to confusion, so see this.

These checkers will be chatty. That is, they'll print extra information to help you diagnose a failure.

Some quick examples

'My function returns a sequential?. What can I check?

contains

The following searches for a subsequence:

     (f) => (contains [4 5 6])

To succeed, f's result must be (1) contiguous and (2) in the same order as in the contains clause. Here are examples:

     [3 4 5 700]   => (contains [4 5 700]) ; true
     [4 700 5]     => (contains [4 5 700]) ; false
     [4 5 'hi 700] => (contains [4 5 700]) ; false

The :in-any-order modifier loosens the second requirement:

     ['hi 700 5 4] => (contains [4 5 700] :in-any-order) ; true
     [4 5 'hi 700] => (contains [4 5 700] :in-any-order) ; still false because 'hi is in the middle

The :gaps-ok modifier loosens the first:

     [4 5 'hi 700]  => (contains [4 5 700] :gaps-ok) ; true
     [4 700 'hi' 5] => (contains [4 5 700] :gaps-ok) ; false because of bad order

The two modifiers can be used at the same time:

     [4 700 5]         => (contains [4 5 700] :gaps-ok :in-any-order) ; true
     [4 5 'hi 700]     => (contains [4 5 700] :in-any-order :gaps-ok) ; true
     [700 'hi 4 5 'hi] => (contains [4 5 700] :in-any-order :gaps-ok) ; true

Another way to indicate :in-any-order is to describe what's contained by a set. The following two are equivalent:

     [700 4 5] => (contains [4 5 700] :in-any-order)
     [700 4 5] => (contains #{4 5 700})

:gaps-ok can be used with a set. (So can :in-any-order, but it has no effect.)

just

A variant of contains, just, will fail if the left-hand-side contains any extra values:

     [1 2 3] => (just [1 2 3])  ; true
     [1 2 3] => (just [1 2 3 4]) ; false

The first of those seems senseless, since you could just use this:

     [1 2 3] => [1 2 3]

However, it's required if you want to use checkers in the expected result:

     [1 2 3] => [odd? even? odd?]  ; false because second-level functions aren't normally treated as checkers.
     [1 2 3] => (just [odd? even? odd?]) ; true

Note that this behavior is not recursive.

just is also useful if you don't care about order:

    [1 3 2] => (just   [1 2 3] :in-any-order)
    [1 3 2] => (just #{1 2 3})

Midje won't complain if you use :gaps-ok with just, but it doesn't have any effect on the result.

has-prefix and has-suffix

has-prefix requires whatever matches the expected result to be at the beginning of the actual result:

    [1 2 3] => (has-prefix   [1 2]) ; true
    [1 2 3] => (has-prefix   [2 1]) ; false - order matters
    [1 2 3] => (has-prefix   [2 1] :in-any-order) ; true
    [1 2 3] => (has-prefix #{2 1}) ; true

As with just, gaps-ok doesn't mean anything.

has-suffix is like has-prefix, except the match has to be at the very end of the actual results.

has

You can apply Clojure's quantification functions (every?, some, and so on) to all the values of sequence:

     (f) => (has some odd?)
     (f) => (has every? odd?)

n-of checkers

Can check if a sequence contains precisely n results, that all match the checker.

     ;; There are n-of checkers from one-of up to ten-of.
     ["a"] => (one-of "a")
     [:k :w] => (two-of keyword?)
     ;; ...
     [1 3 5 7 9 11 13 15 17] => (nine-of odd?)
     ["a" "b" "c" "d" "e" "f" "g" "h" "i" "j"] => (ten-of string?)
     
     ;; to go above ten-of
     (repeat 100 "a") => (n-of "a" 100)

Counts must be exactly n.

     ["a" "a" "a"] =not=> (one-of "a")
     [:k :w :extra :keywords] =not=> (two-of keyword?)

Dropping brackets

Most often, the expected result can be described as a vector. In that case, you can leave off the surrounding brackets. The following two are equivalent:

     (f) => (has-prefix [1 2] :in-any-order)
     (f) => (has-prefix  1 2  :in-any-order)

You can't, however, leave off the brackets if you mean "a singleton list containing a collection". That is, these two are different:

     (f) => (contains   #{1 2}     ; (f) contains 1 and 2, in any order.
     (f) => (contains [ #{1 2} ] ) ; (f) contains the set #{1 2}

You can drop the brackets when you expect two or more collections, since that's unambiguous:

     (f) => (contains #{1 2} #{3 4}) ; (f) contains two sets.

"My function returns a string. What can I check?"

Any checker that makes sense for a sequential?collection makes sense for a string:

     "abc" => (contains "bc")
     "abc" => (contains "ac" :gaps-ok)
     "abc" => (contains "ba" :in-any-order)

     "abc" => (has-suffix "bc")

     "123" => (has every? #(Character/isDigit %))
     "11"  => (two-of \\1)

Strings can also be compared to regular expressions. A regular expression is normally processed as with re-find, not re-matches:

    "123" => #"\d"

If you want to be explicit, you can type this:

     "123" => (contains #"\d")

If you want exact matching, use just:

     "123" => (just #"\d\d\d")

You can also use has-prefix and has-suffix:

    "12x" => (has-prefix #"\d+")
    "x12" => (has-suffix #"\d+")

:in-any-order and :gaps-ok don't make any sense for regular expressions. At least, I don't care to make sense of them.

When a string is used to check a collection, it is applied to each element of the collection. The following two examples mean the same thing:

     ["1" "2" "3"] => (contains   "f"  )
     ["1" "2" "3"] => (contains [ "f" ])

Strings can be compared to individual characters or collections of characters.

     "s"   => (just \s)
     "s"   => (contains \s)
     "as"  => (contains [\s \a] :in-any-order)
     "as"  => (contains  \s \a :in-any-order) ; brackets can be dropped
     "afs" => (contains [\s \a] :in-any-order :gaps-ok)

Note that the reverse is not true: you can't directly compare a collection of characters to an expected string. To do that, convert the string into a vector:

      [\a \b] => (just "ab")      ; false
      [\a \b] => (just (vec "ab")) ; true

All string and character comparisons are case-sensitive. If you want case-insensitive comparisons, use regular expressions:

      "AB" => (just "ab") ; false
      "AB" => #"(?i)ab" ; true

"My function returns a set. What can I check?"

contains checks for a subset, except that it uses Midje's extended equality:

     #{"1" "12" "123"} => (contains [#"1" #"2" #"3"])

Note that you don't have to use a set on the right-hand-side. :in-any-order and gaps-ok don't mean anything for sets, but you can use them if you want to confuse people.

just is set equality, but it also uses Midje's notion of equality:

    #{1 2 3} => (just [odd? odd? even?])

"My function returns a map. What can I check?"

     (f) => (contains {:a 1, :b 2})     ; These two key-value pairs are allowed. Others may be present.
     (f) => (contains {:a even?})      ; :a must be present and have an even value.
     (f) => (just {:a even? :b odd?}) ; The map may only have two entries, :a (whose value is even) and :b (whose value is odd)

Maps may also be represented with pairs or MapEntries:

     (f) => (contains [ [:a 1] [:b 2] ])
     (f) => (contains   [:a 1] [:b 2]  ) ; Brackets can be dropped.

Although, as is shown just above, brackets can be dropped, don't do that if you wish to speak of a single map entry. Consider this:

      (f) => (just [:a :b])

That's interpreted as "(f) contains two entries, :a and :b", not as "(f) contains a single key-value pair."

You can apply Clojure's quantification functions (every?, some, and so on) to all the values of the map:

     (f) => (has every? odd?)

:in-any-order and gaps-ok have no meaning for maps.

"My function returns a record. What can I check?"

Records can be compared to either records of the same type or to maps. A record means you want to check the type as well as the key-value pairs. A map means any type that has the specified key-value pairs is OK.

     (defrecord AB [a b])
     (defrecord AB-different-class [a b])

     (facts "about comparing records to records"
        (AB. 1 2) => (just (AB. 1 2))                      ; true.
        (AB-different-class. 1 2) => (just (AB. 1 2))   ; false: types do not match.
        (AB. 1 2) => (just (AB. odd? even?))              ; true: Checkers are used when found as record values.
        (assoc (AB. 1 2) :c 3) => (contains (AB. 1 2)))  ; true: Left-hand side is still an AB.

     (facts "about comparing records to maps"
        (AB. 1 2) => (contains {:a 1})                   ; true because record type is ignored.
        (AB. 1 2) => (just {:a odd?, :b even?}))         ; true

Records can be compared to map entries, using the same rules as for maps:

      (fact (AB. 1 2) => (just [[:a 1] [:b 2]]))
Clone this wiki locally