RSpec::Expectations Cheat Sheet

Continuing the from the previous post, here is a cheat sheet for RSpec’s assertion library: RSpec::Expectations.

RSpec is a mature, feature-packed testing framework, but the documentation can be difficult to navigate. As an alternative to the official documentation, this cheat sheet contains short example code to demonstrate all the built-in expectation matchers.

These examples were created with RSpec 3.5. All of the examples are passing – there are no examples of failing tests. They cover the functionality in RSpec::Expectations – functionality for expressing assertions inside tests using expect. For examples of RSpec.describe, it, let, etc., see the previous post: RSpec::Core Cheat Sheet.

Basic Matchers

These are the most commonly used matchers.

RSpec.describe 'Common, built-in expectation matchers' do
  example 'Equality' do
    expect('x'+'y').to     eq('xy')   # a == b
    expect('x'+'y').to     eql('xy')  # a.eql?(b)
    expect('x'+'y').not_to be('xy')   # a.equal?(b)
  end

  example 'Strings' do
    expect('abcd').to include('bc')
    expect('abcd').to start_with 'ab'
    expect('abcd').to end_with 'cd'
    expect('abcd').to match /[a-z]+/
  end

  example 'Collections' do
    expect([1, 2, 3]).to include(1, 3)
    expect([1, 2, 3]).to contain_exactly(3, 2, 1) # order not important
    expect({ a: 1, b: 2 }).to include(b: 2)
  end

  example 'Booleans and nil' do
    expect(true).to be true
    expect(false).to be false
    expect('abc').to be_truthy
    expect(nil).to be_falsey
    expect(nil).to be_nil
  end

  example 'Numeric' do
    expect(5).to be > 4
    expect(5).to be >= 4
    expect(5).to be < 6
    expect(5).to be <= 6
    expect(5).to be_between(4, 6).exclusive
    expect(5).to be_between(5, 6).inclusive
    expect(4.99).to be_within(0.02).of(5)
  end

  example 'Errors (exceptions)' do
    expect{ 5 / 0 }.to raise_error(ZeroDivisionError)
    expect{ 5 / 0 }.to raise_error("divided by 0")
    expect{ 5 / 0 }.to raise_error(ZeroDivisionError, "divided by 0")
  end
end

Predicate Matchers

Predicate matchers are a little DSL for calling predicate methods. Predicate methods are methods that:

  1. return a boolean value; and
  2. have a name that ends with ?

Commonly used predicate methods in the Ruby standard library include: Object#nil?, Array#empty?, and Hash#has_key?.

RSpec.describe 'Predicate matchers' do
  example 'Array' do
    expect([]).to be_empty          # [].empty?
  end

  example 'Hash' do
    expect({a: 1}).to have_key(:a)   # {a: 1}.has_key?(:a)
    expect({a: 1}).to have_value(1)  # {a: 1}.has_value?(1)
  end

  example 'Object' do
    expect(5).not_to be_nil             # 'hi'.nil?
    expect(5).to be_instance_of Fixnum  # 5.instance_of?(Fixnum)
    expect(5).to be_kind_of Numeric     # 5.kind_of?(Numeric)
  end
end

Predicate matchers work on all objects, including custom classes.

class Widget
  attr_accessor :name, :cost

  def initialize(name, cost)
    @name = name
    @cost = cost
  end

  def has_cliche_name?
    ['Foo', 'Bar', 'Baz'].include?(@name)
  end

  def hacker?
    @cost == 1337
  end
end

RSpec.describe 'Predicate matchers' do
  example 'With a custom class' do
    widget = Widget.new('Foo', 1337)

    expect(widget).to have_cliche_name
    expect(widget).to be_hacker
    expect(widget).to be_a_hacker
    expect(widget).to be_an_hacker
  end
end

There are a few more predicate matchers, but they are rarely used.

RSpec.describe 'Predicate matchers' do
  example 'Rarely used matchers' do
    expect(4).to respond_to(:odd?)     # 4.respond_to?(:odd?)
    expect(Pathname.new('.')).to exist # pathname.exist?
    expect(1..10).to cover(7)          # (1..10).cover?(7)
  end
end

Advanced Matchers

These are the more complicated matchers, that aren’t as commonly used.

# Add one extra method to the Widget class above,
# for demonstrating change observation.
class Widget
  def fifty_percent_off!
    @cost /= 2
  end
end

RSpec.describe 'Advanced matchers' do
  example 'Change observation' do
    widget = Widget.new('Baz', 80)

    expect{ widget.has_cliche_name? }.not_to change(widget, :name)

    expect{ widget.fifty_percent_off! }.to change(widget, :cost) # 80 -> 40
    expect{ widget.fifty_percent_off! }.to change(widget, :cost).from(40).to(20)
    expect{ widget.fifty_percent_off! }.to change(widget, :cost).by(-10) # 20 -> 10
  end

  example 'Object attributes' do
    # The attributes are a hash of method names to
    # expected return values.
    expect('hi').to have_attributes(length: 2, upcase: 'HI')
  end

  example 'Yielding to blocks' do
    expect{ |b| 5.tap(&b) }.to yield_control
    expect{ |b| 5.tap(&b) }.to yield_with_args(5)
    expect{ |b| 5.tap(&b) }.to yield_with_args(Integer)
    expect{ |b| 5.tap(&b) }.not_to yield_with_no_args
    expect{ |b| 3.times(&b) }.to yield_successive_args(0, 1, 2)
  end

  example 'Output capture' do
    expect{ puts 'hi' }.to output("hi\n").to_stdout
    expect{ $stderr.puts 'hi' }.to output("hi\n").to_stderr
  end

  example 'Throws' do
    # Not to be confused with errors (exceptions).
    # Throw/catch is very rarely used.
    expect{ throw :foo, 5 }.to throw_symbol
    expect{ throw :foo, 5 }.to throw_symbol(:foo)
    expect{ throw :foo, 5 }.to throw_symbol(:foo, 5)
  end

  example 'All elements in a collection' do
    expect([3,5,7]).to all(be_odd)
    expect([3,5,7]).to all(be > 0)
  end

  example 'Compound matchers' do
    expect(5).to be_odd.and be > 0
    expect(5).to be_odd.or be_even

    # These are probably most useful in combination with
    # the `all` matcher (above). For example:
    expect([1,2,3]).to all(be_kind_of(Integer).and be > 0)
  end

  example 'Satisfy' do
    # Test against a custom predicate
    expect(9).to satisfy('be a multiple of 3'){ |x| x % 3 == 0 }
  end
end

Got questions? Comments? Milk?

Shoot an email to tom@rubypigeon.com or hit me up on Twitter (@tom_dalling).

← Previously: RSpec::Core Cheat Sheet

Next up: My Rails Models Are Bloated. Should I Use Concerns? →

Join The Pigeonhole

Don't miss the next post! Subscribe to Ruby Pigeon mailing list and get the next post sent straight to your inbox.