Higher-Order Matchers

All the matchers seen so far are primitives. Now, we’re going to look at higher-order matchers—that is, matchers that you can pass other matchers into. With this technique, you can build up composed matchers that specify exactly the behavior you want.

Collections and Strings

One of the primary tasks of programming, in any language, is dealing with collections, and Ruby is no exception. RSpec ships with six different matchers for dealing with data structures:

All of these matchers also work with strings, with a few minor differences (after all, strings are just collections of characters!). In the following sections, we’ll review the matchers in detail.

include

The include matcher is one of the most flexible, useful matchers RSpec provides. It’s also a key defense against brittleness. By using include rather than a stricter matcher like eq or match, you can specify just the elements you care about. The collection can contain unrelated items, and your tests will still pass.

At its simplest, the include matcher works on any object with an include? method. Strings and arrays both support this method:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​(​'a string'​).to ​include​(​'str'​)
 expect​([1, 2, 3]).to ​include​(3)

For hashes, you can check for the presence of a specific key or key-value pair (passed as a hash):

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 hash = { ​name: ​​'Harry Potter'​, ​age: ​17, ​house: ​​'Gryffindor'​ }
 expect​(hash).to ​include​(​:name​)
 expect​(hash).to ​include​(​age: ​17)

The include matcher accepts a variable number of arguments so that you can specify multiple substrings, array items, hash keys or key-value pairs:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​(​'a string'​).to ​include​(​'str'​, ​'ing'​)
 expect​([1, 2, 3]).to ​include​(3, 2)
 expect​(hash).to ​include​(​:name​, ​:age​)
 expect​(hash).to ​include​(​name: ​​'Harry Potter'​, ​age: ​17)

This works well, but there is a gotcha related to variable numbers of items. Consider this example:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expecteds = [3, 2]
 expect​([1, 2, 3]).to ​include​(expecteds)

This expectation fails, even though the array clearly includes 3 and 2. Here’s the failure message:

 expected [1, 2, 3] to include [3, 2]

The error message gives us a clue. This matcher expects the array to include([3, 2]), rather than to include(3, 2). It would pass if the actual array was something like [1, [3, 2]].

In order for RSpec to look for the individual items, you need to extract them from the array. You can do so by prefixing the expecteds argument with the Ruby splat operator:[84]

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​([1, 2, 3]).to ​include​(*expecteds)

The include matcher is also available as a_collection_including, a_string_including, and a_hash_including, for when you’re passing it into other matchers. As a higher-order matcher, include can also receive matchers as arguments—see Operator Comparisons or Satisfaction for examples.

start_with and end_with

These two matchers are useful when you care about the contents of a string or collection at the start or end but don’t care about the rest. They work exactly the way their names imply:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​(​'a string'​).to start_with(​'a str'​).and end_with(​'ng'​)
 expect​([1, 2, 3]).to start_with(1).and end_with(3)

As the string example shows, you can specify as much or as little of the string as you like. The same applies to arrays; you can check for a sequence of elements at the beginning or end:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​([1, 2, 3]).to start_with(1, 2)
 expect​([1, 2, 3]).to end_with(2, 3)

The same caution about using the Ruby splat operator with include applies to starts_with and ends_with as well.

Following the aliasing pattern we’ve seen elsewhere, these matchers have two aliases each:

You could combine these, for example:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​([​'list'​, ​'of'​, ​'words'​]).to start_with(
  a_string_ending_with(​'st'​)
 ).and end_with(
  a_string_starting_with(​'wo'​)
 )

The outer start_with matcher checks the word ’list’ using the inner a_string_ending_with, and so on.

all

The all matcher is somewhat of an oddity: it is the only built-in matcher that is not a verb, and it is the only one that always takes another matcher as an argument:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 numbers = [2, 4, 6, 8]
 expect​(numbers).to all be_even

This expression does exactly what it says: it expects all the numbers in the array to be even. Here, ‘be_even‘ is a dynamic predicate like the ones we saw in Dynamic Predicates. It calls ‘even?‘ on each element of the array.

One gotcha to be aware of is that, like Enumerable#all?, this matcher passes against an empty array. This can lead to surprises. Consider the following incorrect method for generating a list of numbers:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 def​ self.evens_up_to(n = 0)
  0.upto(n).select(&​:odd?​)
 end
 
 expect​(evens_up_to).to all be_even

This method generates odd numbers instead of evens, but our expectation didn’t fail. We forgot to pass an argument to evens_up_to, and it returned an empty array. One solution is to use a compound matcher to ensure the array is non-empty:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 RSpec::Matchers.define_negated_matcher ​:be_non_empty​, ​:be_empty
 
 expect​(evens_up_to).to be_non_empty.and all be_even

We’re using another RSpec feature, define_negated_matcher, to create a new be_non_empty matcher that’s the opposite of be_empty. We’ll learn more about define_negated_matcher in Negating Matchers.

Now, the expectation correctly flags the broken method as failing:

 expected `[].empty?` to return false, got true

RSpec’s all matcher uses Ruby’s Enumerable#all? under the hood. You may be wondering whether or not RSpec has matchers for the other similar Enumerable methods, like any or none. It doesn’t, because this would lead to nonsensical code such as expect(numbers).to none be_even. Instead, you can build up easier-to-read matchers using to include or not_to include.[85]

match

If you call JSON or XML APIs, you often end up with deeply nested arrays and hashes. The match matcher is a Swiss army knife for this kind of data.

As you did with eq, you provide a data structure that’s laid out like the result you’re expecting. match is more flexible, however. You can substitute a matcher for any array element, or for any hash value, at any level of nesting:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 children = [
  { ​name: ​​'Coen'​, ​age: ​6 },
  { ​name: ​​'Daphne'​, ​age: ​4 },
  { ​name: ​​'Crosby'​, ​age: ​2 }
 ]
 expect​(children).to match [
  { ​name: ​​'Coen'​, ​age: ​a_value > 5 },
  { ​name: ​​'Daphne'​, ​age: ​a_value_between(3, 5) },
  { ​name: ​​'Crosby'​, ​age: ​a_value < 3 }
 ]

When you’re matching against a string, match delegates to String#match, which accepts either a regular expression or a string:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​(​'a string'​).to match(​/str/​)
 expect​(​'a string'​).to match(​'str'​)

Naturally, this matcher has an_object_matching and a_string_matching aliases.

contain_exactly

We’ve seen that match checks data structures more loosely than eq; contain_exactly is even looser. The difference is that match requires a specific order, whereas contain_exactly ignores ordering. For example, both of these expectations pass:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​(children).to contain_exactly(
  { ​name: ​​'Daphne'​, ​age: ​a_value_between(3, 5) },
  { ​name: ​​'Crosby'​, ​age: ​a_value < 3 },
  { ​name: ​​'Coen'​, ​age: ​a_value > 5 }
 )
 
 expect​(children).to contain_exactly(
  { ​name: ​​'Crosby'​, ​age: ​a_value < 3 },
  { ​name: ​​'Coen'​, ​age: ​a_value > 5 },
  { ​name: ​​'Daphne'​, ​age: ​a_value_between(3, 5) }
 )

Like include, contain_exactly receives multiple array elements as separate arguments. It’s also available as a_collection_containing_exactly.

Which Collection Matcher Should I Use?

With a half-dozen collection matchers to pick from, you may wonder which one is the best for your situation. In general, we recommend you use the loosest matcher that still specifies the behavior you care about.

Avoid Overspecification: Favor Loose Matchers

images/aside-icons/info.png

Using a loose matcher makes your specs less brittle; it prevents incidental details from causing an unexpected failure.

images/expectation-flowchart.png

The flowchart provides a quick reference for the different uses of collection and string matchers.

Object Attributes

Some Ruby objects act like fancier versions of hashes. Struct, OpenStruct, and ActiveRecord can all act like buckets for your data, which you read via attributes.

If you need to check an object’s attributes against a template, you can use the have_attributes matcher:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 require ​'uri'
 uri = URI(​'http://github.com/rspec/rspec'​)
 expect​(uri).to have_attributes(​host: ​​'github.com'​, ​path: ​​'/rspec/rspec'​)

This matcher is particularly useful as an argument to another matcher; the an_object_having_attributes form comes in handy here:

11-matchers-included-in-rspec-expectations/02/higher_order_matchers.rb
 expect​([uri]).to ​include​(an_object_having_attributes(​host: ​​'github.com'​))

This comparison is more forgiving than exact equality. Your object can contain additional attributes beyond the ones you specify, and still satisfy the matcher.