Last active
September 15, 2023 13:21
-
-
Save sephraim/e9ef4e11d13927052011826dac37924c to your computer and use it in GitHub Desktop.
RSpec examples #rspec
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Sample reference code for doubles/mocks, stubs, and spies in RSpec | |
# Taken from Kevin Skoglund's RSpec tutorial on Lynda.com | |
describe 'Doubles' do | |
it "allows stubbing methods" do | |
dbl = double("Chant") | |
allow(dbl).to receive(:hey!) | |
expect(dbl).to respond_to(:hey!) | |
end | |
it "allows stubbing a response with a block" do | |
dbl = double("Chant") | |
# When I say 'Hey!', you say 'Ho!' | |
allow(dbl).to receive(:hey!) { "Ho!" } | |
# "Hey!", "Ho!" | |
expect(dbl.hey!).to eq("Ho!") | |
end | |
it "allows stubbing responses with #and_return" do | |
dbl = double("Chant") | |
# This is my perferred syntax | |
# When I say 'Hey!', you say 'Ho!' | |
allow(dbl).to receive(:hey!).and_return("Ho!") | |
# "Hey!", "Ho!" | |
expect(dbl.hey!).to eq("Ho!") | |
end | |
it "allows stubbing multiple methods with hash syntax" do | |
dbl = double("Person") | |
# Note this uses #receive_messages, not #receive | |
allow(dbl).to receive_messages(:full_name => 'Mary Smith', :initials => 'MTS') | |
expect(dbl.full_name).to eq("Mary Smith") | |
expect(dbl.initials).to eq("MTS") | |
end | |
it "allows stubbing with a hash argument to #double" do | |
dbl = double("Person", :full_name => "Mary Smith", :initials => "MTS") | |
expect(dbl.full_name).to eq("Mary Smith") | |
expect(dbl.initials).to eq("MTS") | |
end | |
it "allows stubbing multiple responses with #and_return" do | |
die = double("Die") | |
allow(die).to receive(:roll).and_return(1,5,2,6) | |
expect(die.roll).to eq(1) | |
expect(die.roll).to eq(5) | |
expect(die.roll).to eq(2) | |
expect(die.roll).to eq(6) | |
expect(die.roll).to eq(6) # continues returning last value | |
end | |
context 'with partial test doubles' do | |
it "allows stubbing instance methods on Ruby classes" do | |
time = Time.new(2010, 1, 1, 12, 0, 0) | |
allow(time).to receive(:year).and_return(1975) | |
expect(time.to_s).to eq('2010-01-01 12:00:00 -0500') | |
expect(time.year).to eq(1975) | |
end | |
it "allows stubbing instance methods on custom classes" do | |
class SuperHero | |
attr_accessor :name | |
end | |
hero = SuperHero.new | |
hero.name = 'Superman' | |
expect(hero.name).to eq('Superman') | |
allow(hero).to receive(:name).and_return('Clark Kent') | |
expect(hero.name).to eq('Clark Kent') | |
end | |
it "allows stubbing class methods on Ruby classes" do | |
fixed = Time.new(2010, 1, 1, 12, 0, 0) | |
allow(Time).to receive(:now).and_return(fixed) | |
expect(Time.now).to eq(fixed) | |
expect(Time.now.to_s).to eq('2010-01-01 12:00:00 -0500') | |
expect(Time.now.year).to eq(2010) | |
end | |
it "allows stubbing database calls a mock object" do | |
class Customer | |
attr_accessor :name | |
def self.find | |
# database lookup, returns one object | |
end | |
end | |
dbl = double('Mock Customer') | |
allow(dbl).to receive(:name).and_return('Bob') | |
allow(Customer).to receive(:find).and_return(dbl) | |
customer = Customer.find | |
expect(customer.name).to eq('Bob') | |
end | |
it "allows stubbing database calls with many mock objects" do | |
class Customer | |
attr_accessor :name | |
def self.all | |
# database lookup, returns array of objects | |
end | |
end | |
c1 = double('First Customer', :name => 'Bob') | |
c2 = double('Second Customer', :name => 'Mary') | |
allow(Customer).to receive(:all).and_return([c1,c2]) | |
customers = Customer.all | |
expect(customers[1].name).to eq('Mary') | |
end | |
end | |
context 'with message expectations' do | |
it "expects a call and allows a response" do | |
dbl = double("Chant") | |
expect(dbl).to receive(:hey!).and_return("Ho!") | |
dbl.hey! | |
end | |
it "does not matter which order" do | |
dbl = double("Multi-step Process") | |
expect(dbl).to receive(:step_1) | |
expect(dbl).to receive(:step_2) | |
dbl.step_2 | |
dbl.step_1 | |
end | |
it "works with #ordered when order matters" do | |
dbl = double("Multi-step Process") | |
expect(dbl).to receive(:step_1).ordered | |
expect(dbl).to receive(:step_2).ordered | |
dbl.step_1 | |
dbl.step_2 | |
end | |
end | |
context 'with argument constraints' do | |
it "expects arguments will match" do | |
dbl = double("Customer List") | |
expect(dbl).to receive(:sort).with('name') | |
dbl.sort('name') | |
end | |
it "passes when any arguments are allowed" do | |
dbl = double("Customer List") | |
# The default if you don't use #with | |
expect(dbl).to receive(:sort).with(any_args) | |
dbl.sort('name') | |
end | |
it "works the same with multiple arguments" do | |
dbl = double("Customer List") | |
expect(dbl).to receive(:sort).with('name', 'asc', true) | |
dbl.sort('name', 'asc', true) | |
end | |
it "allows contraining only some arguments" do | |
dbl = double("Customer List") | |
expect(dbl).to receive(:sort).with('name', anything, anything) | |
dbl.sort('name', 'asc', true) | |
end | |
it "allows using other matchers" do | |
dbl = double("Customer List") | |
expect(dbl).to receive(:sort).with( | |
a_string_starting_with('n'), | |
an_object_eq_to('asc') | an_object_eq_to('desc'), | |
boolean | |
) | |
dbl.sort('name', 'asc', true) | |
end | |
end | |
context 'with message count constraints' do | |
it "allows constraints on message count" do | |
class Cart | |
def initialize | |
@items = [] | |
end | |
def add_item(id) | |
@items << id | |
end | |
def restock_item(id) | |
@items.delete(id) | |
end | |
def empty | |
@items.each {|id| restock_item(id) } | |
end | |
end | |
cart = Cart.new | |
cart.add_item(35) | |
cart.add_item(178) | |
expect(cart).to receive(:restock_item).twice | |
cart.empty | |
end | |
it "allows using at_least/at_most" do | |
post = double('BlogPost') | |
expect(post).to receive(:like).at_least(3).times | |
post.like(:user => 'Bob') | |
post.like(:user => 'Mary') | |
post.like(:user => 'Ted') | |
post.like(:user => 'Jane') | |
end | |
end | |
context 'with spying abilities' do | |
it "can expect a call after it is received" do | |
dbl = spy("Chant") | |
allow(dbl).to receive(:hey!).and_return("Ho!") | |
dbl.hey! | |
expect(dbl).to have_received(:hey!) | |
end | |
it "can use message constraints" do | |
dbl = spy("Chant") | |
allow(dbl).to receive(:hey!).and_return("Ho!") | |
dbl.hey! | |
dbl.hey! | |
dbl.hey! | |
expect(dbl).to have_received(:hey!).with(no_args).exactly(3).times | |
end | |
it "can expect any message already sent to a declared spy" do | |
customer = spy("Customer") | |
# Notice that we don't stub :send_invoice | |
# allow(customer).to receive(:send_invoice) | |
customer.send_invoice | |
expect(customer).to have_received(:send_invoice) | |
end | |
it "can expect only allowed messages on partial doubles" do | |
class Customer | |
def send_invoice | |
true | |
end | |
end | |
customer = Customer.new | |
# Must stub :send_invoice to start spying | |
allow(customer).to receive(:send_invoice) | |
customer.send_invoice | |
expect(customer).to have_received(:send_invoice) | |
end | |
context 'using let and a before hook' do | |
let(:order) do | |
spy('Order', :process_line_items => nil, | |
:charge_credit_card => true, | |
:send_confirmation_email => true) | |
end | |
before(:example) do | |
order.process_line_items | |
order.charge_credit_card | |
order.send_confirmation_email | |
end | |
it 'calls #process_line_items on the order' do | |
expect(order).to have_received(:process_line_items) | |
end | |
it 'calls #charge_credit_card on the order' do | |
expect(order).to have_received(:charge_credit_card) | |
end | |
it 'calls #send_confirmation_email on the order' do | |
expect(order).to have_received(:send_confirmation_email) | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Sample reference code for expectation matchers in RSpec | |
# Taken from Kevin Skoglund's RSpec tutorial on Lynda.com | |
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 = '[email protected]' | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment