RSpec Cheat Sheet


Blog Post Banner

Some useful Rspec matcher.


require 'rails_helper'

RSpec.describe TodosController, :type => :controller do

  describe "GET #index" do
  #describe "POST #create" do
  #describe "GET #show" do
  #describe "PATCH #update" do (or PUT #update)
  #describe "DELETE #destroy" do
  #describe "GET #new" do
  #describe "GET #edit" do
  
    
    # NORMALLY, you DO NOT want render_views, or you only want to call it in
    # a single context.
    # More on render_views: 
    # https://www.relishapp.com/rspec/rspec-rails/v/3-1/docs/controller-specs/render-views
    render_views # ONLY have this if you're certain you need it
    
    it "reads like a sentence (almost)" do
      
      # Available HTTP methods: post, get, patch, put, delete, head
      get :index
      
      params = { id: 123 }

      get :edit, params # old non-kwarg style
      get :edit, params: params # new kwarg style
      
      params = { widget: { description: 'Hello World' } }
      params.merge!(format: :js) # Specify format for AJAX/JS responses (e.g. create.js.erb view)
      
      post :create, params # old non-kwarg style
      post :create, params: params # new kwarg style
      
      # All optional kwargs:
      post :create,
        params: {}, # hash with HTTP parameters, may be nil
        body: "...", # request body string, appropriately encoded (application/x-www-form-urlencoded or multipart/form-data)
        session: {}, # hash of parameters to store in session, may be nil.
        flash: {}, # hash of parameters to store in flash, may be nil.
        format: :json, # Request format (string or symbol), defaults to nil.
        as: :json # Content type must be symbol that corresponds to a mime type, defaults to nil.
      
      # Testing 404s in controllers (assuming default Rails handling of RecordNotFound)
      expect { delete :destroy, { id: 'unknown' } }.to raise_error(ActiveRecord::RecordNotFound)
      
      # Rails `:symbolized` status codes at end of each status code page at http://httpstatus.es/
      expect(response).to have_http_status(:success) # 200
      expect(response).to have_http_status(:forbidden) # 403
      
      expect(response).to redirect_to foo_path
      expect(response).to render_template(:template_filename_without_extension)
      expect(response).to render_template(:destroy)
      
      # Need response.body? Requires render_views call outside "it" block (see above & read given URL)
      expect(response.body).to match /Bestsellers/ 
      expect(response.body).to include "Bestsellers"
      
      expect(response.headers["Content-Type"]).to eq "text/html; charset=utf-8"
      expect(response.headers["Content-Type"]).to eq "text/javascript; charset=utf-8"
      
      # assigns(:foobar) accesses the @foobar instance variable
      # the controller method made available to the view
      
      # Think of assigns(:widgets) as @widgets in the controller method
      expect(assigns(:widgets)).to eq([widget1, widget2, widget3])

      # Think of assigns(:product) as @product in the controller method
      expect(assigns(:product)).to eq(bestseller)
      expect(assigns(:cat)).to be_cool # cat.cool is a boolean, google "rspec predicate matchers"
      expect(assigns(:employee)).to be_a_new(Employee) 
      
    
      # Asserting flash messages
      expect(flash[:notice]).to eq "Congratulations on buying our stuff!"
      expect(flash[:error]).to eq "Buying our stuff failed :-("
      expect(flash[:alert]).to eq "You didn't buy any of our stuff!!!"
      
      # Query the db to assert changes persisted
      expect(Invoice.count).to eq(1)
      
      # Reload from db an object fetched in test setup when its record in db
      # is updated by controller method, otherwise you're testing stale data
      employee.reload
      invoice.reload
      product.reload
      widget.reload

    end
    
  end

end

===============


describe 'Expectation Matchers' do
  
  describe 'equivalence matchers' do

    it 'will match loose equality with #eq' do
      a = "2 cats"
      b = "2 cats"
      expect(a).to eq(b)
      expect(a).to be == b      # synonym for #eq

      c = 17
      d = 17.0
      expect(c).to eq(d)        # different types, but "close enough"
    end
    
    it 'will match value equality with #eql' do
      a = "2 cats"
      b = "2 cats"
      expect(a).to eql(b)       # just a little stricter

      c = 17
      d = 17.0
      expect(c).not_to eql(d)   # not the same, close doesn't count
    end

    it 'will match identity equality with #equal' do
      a = "2 cats"
      b = "2 cats"
      expect(a).not_to equal(b) # same value, but different object

      c = b
      expect(b).to equal(c)     # same object
      expect(b).to be(c)        # synonym for #equal
    end
    
  end
  
  describe 'truthiness matchers' do

    it 'will match true/false' do
      expect(1 < 2).to be(true)       # do not use 'be_true'
      expect(1 > 2).to be(false)      # do not use 'be_false'

      expect('foo').not_to be(true)   # the string is not exactly true
      expect(nil).not_to be(false)    # nil is not exactly false
      expect(0).not_to be(false)      # 0 is not exactly false
    end
    
    it 'will match truthy/falsey' do
      expect(1 < 2).to be_truthy
      expect(1 > 2).to be_falsey

      expect('foo').to be_truthy      # any value counts as true
      expect(nil).to be_falsey        # nil counts as false
      expect(0).not_to be_falsey      # but 0 is still not falsey enough
    end
    
    it 'will match nil' do
      expect(nil).to be_nil
      expect(nil).to be(nil)          # either way works
      
      expect(false).not_to be_nil     # nil only, just like #nil?
      expect(0).not_to be_nil         # nil only, just like #nil?
    end

  end
  
  describe 'numeric comparison matchers' do
  
    it 'will match less than/greater than' do
      expect(10).to be > 9
      expect(10).to be >= 10
      expect(10).to be <= 10
      expect(9).to be < 10
    end

    it 'will match numeric ranges' do
      expect(10).to be_between(5, 10).inclusive
      expect(10).not_to be_between(5, 10).exclusive
      expect(10).to be_within(1).of(11)
      expect(5..10).to cover(9)
    end

  end
  
  describe 'collection matchers' do
    
    it 'will match arrays' do
      array = [1,2,3]
      
      expect(array).to include(3)
      expect(array).to include(1,3)

      expect(array).to start_with(1)
      expect(array).to end_with(3)
      
      expect(array).to match_array([3,2,1])
      expect(array).not_to match_array([1,2])

      expect(array).to contain_exactly(3,2,1)   # similar to match_array
      expect(array).not_to contain_exactly(1,2) # but use individual args
    end

    it 'will match strings' do
      string = 'some string'
      
      expect(string).to include('ring')
      expect(string).to include('so', 'ring')
      
      expect(string).to start_with('so')
      expect(string).to end_with('ring')
    end
    
    it 'will match hashes' do
      hash = {:a => 1, :b => 2, :c => 3}
      
      expect(hash).to include(:a)
      expect(hash).to include(:a => 1)
      
      expect(hash).to include(:a => 1, :c => 3)
      expect(hash).to include({:a => 1, :c => 3})
      
      expect(hash).not_to include({'a' => 1, 'c' => 3})
    end
    
  end
  
  describe 'other useful matchers' do
  
    it 'will match strings with a regex' do
      # This matcher is a good way to "spot check" strings
      string = 'The order has been received.'
      expect(string).to match(/order(.+)received/)

      expect('123').to match(/\d{3}/)
      expect(123).not_to match(/\d{3}/) # only works with strings

      email = 'someone@somewhere.com'
      expect(email).to match(/\A\w+@\w+\.\w{3}\Z/)
    end

    it 'will match object types' do
      expect('test').to be_instance_of(String)
      expect('test').to be_an_instance_of(String) # alias of #be_instance_of

      expect('test').to be_kind_of(String)
      expect('test').to be_a_kind_of(String)  # alias of #be_kind_of
      expect('test').to be_a(String)          # alias of #be_kind_of
      expect([1,2,3]).to be_an(Array)         # alias of #be_kind_of
    end
    
    it 'will match objects with #respond_to' do
      string = 'test'
      expect(string).to respond_to(:length)
      expect(string).not_to respond_to(:sort)
    end

    it 'will match class instances with #have_attributes' do
      class Car
        attr_accessor :make, :year, :color
      end
      car = Car.new
      car.make = 'Dodge'
      car.year = 2010
      car.color = 'green'

      expect(car).to have_attributes(:color => 'green')
      expect(car).to have_attributes(
        :make => 'Dodge', :year => 2010, :color => 'green'
      )
    end
    
    it 'will match anything with #satisfy' do
      # This is the most flexible matcher
      expect(10).to satisfy do |value|
        (value >= 5) && (value <=10) && (value % 2 == 0)
      end
    end

  end
  
  describe 'predicate matchers' do
    
    it 'will match be_* to custom methods ending in ?' do
      # drops "be_", adds "?" to end, calls method on object
      # Can use these when methods end in "?", require no arguments, 
      # and return true/false.

      # with built-in methods
      expect([]).to be_empty                # [].empty?
      expect(1).to be_integer               # 1.integer?
      expect(0).to be_zero                  # 0.zero?
      expect(1).to be_nonzero               # 1.nonzero?
      expect(1).to be_odd                   # 1.odd?
      expect(2).to be_even                  # 1.even?
      
      # be_nil is actually an example of this too
      
      # with custom methods
      class Product
        def visible?; true; end
      end
      product = Product.new

      expect(product).to be_visible         # product.visible?
      expect(product.visible?).to be true   # exactly the same as this
    end

    it 'will match have_* to custom methods like has_*?' do
      # changes "have_" to "has_", adds "?" to end, calls method on object
      # Can use these when methods start with "has_", end in "?", 
      # and return true/false. Can have arguments, but not required.

      # with built-in methods
      hash = {:a => 1, :b => 2}
      expect(hash).to have_key(:a)          # hash.has_key?
      expect(hash).to have_value(2)         # hash.has_value?

      # with custom methods
      class Customer
        def has_pending_order?; true; end
      end
      customer = Customer.new

      expect(customer).to have_pending_order  # customer.has_pending_order?
      expect(customer.has_pending_order?).to be true  # same as this
    end
    
  end
  
  describe 'observation matchers' do
    # Note that all of these use "expect {}", not "expect()".
    # It is a special block format that allows a 
    # process to take place inside of the expectation.
    
    it 'will match when events change object attributes' do
      # calls the test before the block, 
      # then again after the block
      array = []
      expect { array << 1 }.to change(array, :empty?).from(true).to(false)

      class WebsiteHits
        attr_accessor :count
        def initialize; @count = 0; end
        def increment; @count += 1; end
      end
      hits = WebsiteHits.new
      expect { hits.increment }.to change(hits, :count).from(0).to(1)
    end
    
    it 'will match when events change any values' do
      # calls the test before the block, 
      # then again after the block

      # notice the "{}" after "change", 
      # can be used on simple variables
      x = 10
      expect { x += 1 }.to change {x}.from(10).to(11)
      expect { x += 1 }.to change {x}.by(1)
      expect { x += 1 }.to change {x}.by_at_least(1)
      expect { x += 1 }.to change {x}.by_at_most(1)
      
      # notice the "{}" after "change", 
      # can contain any block of code
      z = 11
      expect { z += 1 }.to change { z % 3 }.from(2).to(0)

      # Must have a value before the block
      # Must change the value inside the block
    end
    
    it 'will match when errors are raised' do
      # observes any errors raised by the block
      
      expect { raise StandardError }.to raise_error
      expect { raise StandardError }.to raise_exception

      expect { 1 / 0 }.to raise_error(ZeroDivisionError)
      expect { 1 / 0 }.to raise_error.with_message("divided by 0")
      expect { 1 / 0 }.to raise_error.with_message(/divided/)
      
      # Note that the negative form does 
      # not accept arguments
      expect { 1 / 1 }.not_to raise_error
    end
    
    it 'will match when output is generated' do
      # observes output sent to $stdout or $stderr

      expect { print('hello') }.to output.to_stdout
      expect { print('hello') }.to output('hello').to_stdout
      expect { print('hello') }.to output(/ll/).to_stdout

      expect { warn('problem') }.to output(/problem/).to_stderr
    end
    
  end
  
  describe 'compound expectations' do
    
    it 'will match using: and, or, &, |' do
      expect([1,2,3,4]).to start_with(1).and end_with(4)
      
      expect([1,2,3,4]).to start_with(1) & include(2)

      expect(10 * 10).to be_odd.or be > 50
      
      array = ['hello', 'goodbye'].shuffle
      expect(array.first).to eq("hello") | eq("goodbye")
    end
    
  end
  
  describe 'composing matchers' do
    # some matchers accept matchers as arguments. (new in rspec3)
    
    it 'will match all collection elements using a matcher' do
      array = [1,2,3]
      expect(array).to all( be < 5 )
    end
    
    it 'will match by sending matchers as arguments to matchers' do
      string = "hello"
      expect { string = "goodbye" }.to change { string }.
        from( match(/ll/) ).to( match(/oo/) )

      hash = {:a => 1, :b => 2, :c => 3}
      expect(hash).to include(:a => be_odd, :b => be_even, :c => be_odd)
      expect(hash).to include(:a => be > 0, :b => be_within(2).of(4))
    end
    
    it 'will match using noun-phrase aliases for matchers' do
      # These are built-in aliases that make 
      # specs read better by using noun-based 
      # phrases instead of verb-based phrases.
      
      # valid but awkward example
      fruits = ['apple', 'banana', 'cherry']
      expect(fruits).to start_with( start_with('a') ) & 
        include( match(/a.a.a/) ) & 
        end_with( end_with('y') )
      
      # improved version of the previous example
      # "start_with" becomes "a_string_starting_with"
      # "end_with" becomes "a_string_ending_with"
      # "match" becomes "a_string_matching"
      fruits = ['apple', 'banana', 'cherry']
      expect(fruits).to start_with( a_string_starting_with('a') ) & 
        include( a_string_matching(/a.a.a/) ) & 
        end_with( a_string_ending_with('y') )


      # valid but awkward example
      array = [1,2,3,4]
      expect(array).to start_with( be <= 2 ) |
        end_with( be_within(1).of(5) )

      # improved version of the previous example
      # "be <= 2" becomes "a_value <= 2"
      # "be_within" becomes "a_value_within"
      array = [1,2,3,4]
      expect(array).to start_with( a_value <= 2 ) |
        end_with( a_value_within(1).of(5) )
    end
    
  end

end