Created
November 5, 2010 09:29
-
-
Save dnagir/663876 to your computer and use it in GitHub Desktop.
RSpec 2 syntax cheat sheet by example
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
# RSpec 2.0 syntax Cheet Sheet by http://ApproachE.com | |
# defining spec within a module will automatically pick Player::MovieList as a 'subject' (see below) | |
module Player | |
describe MovieList, "with optional description" do | |
it "is pending example, so that you can write ones quickly" | |
it "is already working example that we want to suspend from failing temporarily" do | |
pending("working on another feature that temporarily breaks this one") | |
# actual test code is here, will never be reached | |
end | |
it "is pending when failing" do | |
pending "This will be marked as pending when the block will fail, otherwise (on success) will fail telling 'Why am I pending if I pass?'" do | |
1.should == 2 # will mark example as pending | |
2.should == 2 # will fail asking to remove pending status of example | |
end | |
end | |
# this will automatically generate name of the example based on the expectations inside it ~ 'it' with no description | |
specify { [1,2,3].should have(3).items } | |
#any helper methods, before/after, modules etc declared in the outer group are available in the inner group. | |
describe "outer" do | |
before(:each) { puts "first" } | |
describe "inner" do | |
before(:each) { puts "second" } | |
it { puts "third"} | |
after(:each) { puts "fourth" } | |
end | |
after(:each) { puts "fifth" } | |
end | |
# 'describe' and 'context' are equivalent | |
# I prefer to use 'context' for defining an 'environment' | |
context "when first created" do | |
it "is empty" do | |
movie_list = MovieList.new | |
movie_list.should be_empty | |
end | |
end | |
# I prefer to use 'describe' for nouns, verbs; defining a nested set of specifications | |
describe "forward" do | |
it "should jump to a next movie" do | |
next_movie = MovieList.new(2).forward | |
next_movie.track_number.should == 2 | |
end | |
end | |
end | |
it "will have default subject that corresponds to the instance of first parameter in 'describe'" do | |
subject.class.should be == MovieList | |
end | |
# unless subject is set explicitly | |
subject { MovieList.new(10) } # approximately similar to 'before(:each)' | |
# no need to use 'subject.should', use 'should' | |
specify { should have(10).items } # same as below | |
specify { subject.should have(10).items } | |
# similar to specify { subject.track_number.should == 1} | |
its(:track_number) { should == 1 } | |
context "specs set-up" do | |
# we can run setup before each examle, or all of them | |
before(:each) do | |
@new_on_each_example = YourObject.new | |
end | |
before do | |
@new_on_each_spec_less_verbose = YourObject.new | |
end | |
before(:all) do | |
# Avoid using it as it will bring the 'shared state' into unit tests | |
@same_instance_for_all_examples_within_the_context = YourObject.new | |
end | |
it "can access attributes defined in 'before'" do | |
@new_on_each_example.should_not be_nil | |
@same_instance_for_all_examples_within_the_context.should_not be_nil | |
end | |
# cleanup code can be run the same way using 'after' instead of 'before' | |
# Avoid using 'after' | |
# we can wrap examples: before + after + manual handling | |
# In most cases 'before' + 'after' will work better. | |
around do |example| | |
DB.transaction { example.run } | |
# should handle errors manually, so do not do something like: | |
# DB.start_transaction | |
# example.run | |
# DB.rollback_transaction | |
end | |
it "should run within a transaction" do | |
MovieList.new.save! | |
end | |
let(:new_on_each_example) { ObjectPerExample.new } | |
it "can use method defined by 'let'" do | |
new_on_each_example.should_not be_nil | |
# the object is memoized, so | |
new_on_each_example.should == new_on_each_example | |
end | |
# defining helper methods within context may be more useful than setup | |
def forward(times) do | |
list = MoviewList.new(10) | |
list.forward(times).track_number | |
end | |
it "can use it multiple times" do | |
forward(1).should == 1 | |
forward(2).should == 2 | |
forward(10).should == 1 | |
end | |
# using 'yield' with helper methods | |
def given_thing_with(options) | |
yield Thing.new do |thing| | |
thing.set_status(options[:status]) | |
end | |
end | |
it "should do something when ok" do | |
given_thing_with(:status => 'ok') do |thing| | |
thing.do_fancy_stuff(1, true, :move => 'left', :obstacles => nil) | |
end | |
end | |
# helpers can come from modules | |
module Helpers | |
def shared_help | |
[1,2,3] | |
end | |
end | |
include Helpers | |
it "can use helpers from Module" do | |
shared_help.should == [1,2,3] | |
end | |
# or this module can be included for ALL example groups automatically during configuration: | |
# RSpec.configure do |config| | |
# config.include Helpers | |
# end | |
end | |
context "built-it stubbing, faking, mocking" do | |
it "can stub" do | |
source = double('source') | |
source.stub(:fetch) { [1,2,3,4,5] } | |
source.stub(:fetch_from).and_return([1,2]) # other way | |
MovieList.stub(:find).and_return(MovieList.new) # stub class method | |
implementing = double('source') | |
implementing.stub(:fetch) do |count| | |
count == 5 ? [1,2,3] : [4,5,6,7] # provide stub logic here, easy to use for Fakes | |
end | |
# easily stub chains of calls | |
Blog.stub_chain(:posts, :published, :recent).and_return([1,2,3]) | |
Blog.posts.published.recent.should == [1,2,3] | |
end | |
it "can ignore non-expected method calls (NullObject pattern)" do | |
source = double('source', :url => 'http://example.com').as_null_object | |
source.any_method_call_onwill_return_nil.should be_nil | |
# the source mock object will record the 'any_method_call_onwill_return_nil' message internally though | |
end | |
it "can set expectations" do | |
source = double('source') | |
# arguments | |
source.should_receive(:fetch).with(10, "abc").and_return([1,2]) # expecting arguments (10, "abc") otherwise failing | |
source.should_receive(:fetch).with(instance_of(Integer), "abc").and_return([1,2]) # don't care about 1st argument as long as it is Integer | |
source.should_receive(:fetch).with(10, anything).and_return([1,2]) # don't care about 2nd argument at all | |
source.should_receive(:fetch).with(any_args) # same as not using 'with' - don't care about arguments | |
source.should_receive(:fetch).with(no_args) # 0 arguments, otherwise fail | |
source.should_receive(:fetch).with(hash_including(:count => 10, :url => 'abc')) # arg should be Hash with all the values mentioned | |
source.should_receive(:fetch).with(hash_not_including(:timeout => 5)) # arg should be Hash that contains no ':timout=>5' | |
source.should_receive(:fetch).with(anything, /example/) # 2nd arg shuold match RegEx | |
source.should_receive(:fetch).and_return([1], [1,2], [1,2,3]) # 1st call - [1], 2nd - [1,2], 3rd - [1,2,3], 4th - [1] and so on ... | |
# expectation overrides stub | |
source.stub(:fetch).and_return([1,2]) # will return [1,2] when called | |
source.should_recieve(:fetch).and_return([3,4]) # prev been overriden and will return [3,4] | |
# raising/throwing | |
source.should_receive(:fetch).and_raise # raise Exception | |
source.should_receive(:fetch).and_raise(ZeroDivisionError) # raise ZeroDivisionError | |
source.should_receive(:fetch).and_raise(Exception.new('instance of aexception')) # raise given exception | |
source.should_receive(:fetch).and_throw(:zero) # thro :zero | |
# order | |
source.should_receive(:first).ordered # order matters in relation to others marked as ordered | |
source.should_receive(:dosnt_matter) # don't care about order as long as it is called | |
source.should_receive(:second).ordered # must be called after 'first' | |
# order is not enforced across different objects: | |
double('a').should_receive(:a).ordered # not related to the next one | |
double('b').should_receive(:b).ordered # not related to the prev one | |
# how many times? | |
source.should_recieve(:fetch).exactly(1)times | |
source.should_recieve(:fetch).at_most(5)times | |
source.should_recieve(:fetch).at_least(2)times | |
source.should_recieve(:fetch).twice | |
source.should_recieve(:fetch).once | |
# negative expectations | |
source.should_recieve(:fetch).never | |
source.should_recieve(:fetch).exactly(0)times | |
source.should_not_recieve(:fetch) | |
list = MovieList.new(source) | |
# if source.fetch has not been called, then example will fail | |
end | |
context "custom expectations" do | |
# define custom expection class somewhere | |
class GreaterThanMatcher | |
def initialize(expected) | |
@expected = expected | |
end | |
def description | |
# will generate proper failure message and name of the example | |
"a number greater than #{@expected}" | |
end | |
def ==(actual) | |
# this will be called from | |
actual > @expected | |
end | |
end | |
# add this method to the RSpec (see set-up for global configuration) | |
def greater_than(floor) | |
GreaterThanMatcher.new(floor) | |
end | |
it "can be used in expectations" do | |
subject.should_recieve(:forward).with(greater_than 3) | |
subject.forward(5) | |
end | |
end # custom matchers | |
end # mocking | |
# set of same examples shared accross multiple specs | |
# shared_examples_for should be in a separate file and defined outside of 'describe'/'context' | |
shared_examples_for "any pizza" do | |
it "tastes really good" do | |
@pizza.should taste_really_good | |
end | |
end | |
# to include the shared examples, into example groups: | |
# it will assume @pizza instance variable is available here | |
it_behaves_like "any pizza" | |
context 'defining examples dynamically - everybody knows that :)' do | |
{2 => 4, 3 => 6, 10 => 20}.each do |input, output| | |
it "#{input} * 2 should be equal to #{output}" do | |
(input * 2).should == output | |
# will produce examples: | |
# - 2 * 2 should be equal to 4 | |
# - 3 * 2 should be equal to 6 | |
# - 10 * 2 should be equal to 20 | |
end | |
end | |
end | |
context "matchers" do | |
it "shows built-in matchers" do | |
# TODO: describe ===, eql, equal | |
1.should == 1 | |
1.should_not == 2 # NOT 1.should != 2 | |
1.should_not equal(2) # same as above | |
1.should_not == 2 | |
5.should be > 3 | |
5.should be <= 5 | |
(1.251).should be_close(1.25, 0.005) | |
(1.251).should be_within(0.005).of 1.25 # >= RSpec 2.1 | |
"reg exp".should =~ /exp/ | |
[1,2].should include(1) | |
1.should respond_to(:to_s) | |
true.should be_true | |
0.should be_true | |
"this".should be_true | |
lambda { Object.new.explodde! }.should raise_error(NameError) | |
# nothing fits | |
5.should satisfy { |it| it == 5 } | |
end | |
it "shows cool things" do | |
count = 1 | |
expect { | |
count = 3 | |
}.to change { count }.by(2) | |
expect { | |
# not changing | |
}.to_not change { count } | |
count = 1 | |
expect { | |
count = 3 | |
}.to change { count }.to(3) | |
count = 1 | |
expect { | |
count = 3 | |
}.to change { count }from(1).to(3) | |
# raise-rescue - exception handling | |
expect {2 / 0}.to raise_error("divided by 0") | |
expect {2 / 0}.to raise_error(/by 0/) | |
expect {2 / 0}.to raise_error(ZeroDivisionError) | |
# try-catch - expected circumstance handling | |
lambda { throw :room_is_full }.should throw_symbol(:room_is_full) | |
# predicates | |
nil.should be_nil #call nil.nil? | |
[].should be_empty # calls [].empty? | |
[1,2,3].should_not be_empty # calls [1,2,3].empty | |
# convert anything that begins with have_ to a predicate on the target object beginning with has_ | |
{:id => 1}.has_key?(:id).should == true | |
# can be written as | |
{:id => 1}.should have_key(:id) # calls {:id => 1}.has_key?(:id) | |
# collections | |
obj = {} | |
def obj.numbers | |
[1,2,3,4] | |
end | |
obj.should have(4).numbers # calls obj.numbers.length | |
[1,2,3,4].should have(4).items # 'items' is 'reserved' to say "ensure number of items on the collection" | |
[1,2,3,4].should be_any {|n| n % 2 == 0} # [1,2,3,4].any? {|n| n %% 2 == 0}.should be_true | |
"stringy".should have(7).charaters # same as items, just syntactic sugar | |
[1,2,3,4].should have_exactly(24).items # same as 'have' | |
obj.should have_at_least(3).numbers | |
end | |
end # built-in matchers | |
context "custom matchers" do | |
# TODO: describe multiple ways | |
#define class | |
class SimilarTo | |
# mandatory - link to the object under test | |
def initialize(it) | |
# object under test | |
@it = it | |
end | |
# mandatory - check the positive condition | |
def matches?(that) | |
@that = that # save to use it in messages | |
@that.to_s.downcase.should == @it.to_s.downcase | |
end | |
# optional - opoosite to mathch? | |
def does_not_matche?(that) | |
result = !matches?(that) | |
@that, @it = @it, @that # swap for negative condition or additionally cusomtize messages | |
result # don't forget to return | |
end | |
# mandatory | |
def failure_message_for_should | |
"expected #{@it} to be similar to #{@that}" | |
end | |
# optional | |
def failure_message_for_should_not | |
"expected #{@it} to be different from #{@that}" | |
end | |
#optional | |
def description | |
"#{@it} should be similar to #{@that}" | |
end | |
end | |
#define method on example (see set-up to incude in all examples) | |
def similar_to(that) | |
SimilarTo.new(that) | |
end | |
end # custom matchers | |
context "macros" do | |
module ControllerMacros | |
def should_render(template) | |
it "should render the #{template} template" do | |
do_request | |
response.should render_template(template) | |
end | |
end | |
def should_assign(hash) | |
variable_name = hash.keys.first | |
model, method = hash[variable_name] | |
model_access_method = [model, method].join('.') | |
it "should assign @#{variable_name} => #{model_access_method}" do | |
expected = "the value returned by #{model_access_method}" | |
model.should_receive(method).and_return(expected) | |
do_request | |
assigns[variable_name].should == expected | |
end | |
end | |
def get(action) | |
define_method :do_request do | |
get action | |
end | |
yield | |
end | |
end | |
RSpec.configure do |config| | |
config.use_transactional_fixtures = true | |
config.use_instantiated_fixtures = false | |
config.fixture_path = RAILS_ROOT + '/spec/fixtures/' | |
config.extend(ControllerMacros, :type => :controller) | |
end | |
end # macros | |
end # module |
Time for a 2.14 update! (expect & allow)
Thanks, amazing work!, btw there are some typos It should be "should_receive" instead of "should_recieve" on some places
Thanks man! this is exactly what I was looking for!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a great resource! However, I was wondering if you had any tips on mocking modules/classes with submodules/subclasses? Specifically, I am attempting to mock the Hallon gem and it has
::Session
and::Player
that I need to mock as my function receives just the Hallon module wrapper. So mydouble
needs to have::Session
and::Player
so that they can be stubbed like thus:I'm relatively new to Ruby and rspec so bear with me if I am approaching this issue from the completely wrong direct. Any feedback or ideas?