rspec matchers
Zaharie Marius - 06-03-2015
1 / 9
1. Composable matchers
Example:
#background_worker_spec.rb
describeBackgroundWorkerdo
it'putsenqueuedjobsontothequeueinorder'do
worker=BackgroundWorker.new
worker.enqueue(:klass=>"Class1",:id=>37)
worker.enqueue(:klass=>"Class2",:id=>42)
expect(worker.queue).tomatch[
a_hash_including(:klass=>"Class1",:id=>37),
a_hash_including(:klass=>"Class2",:id=>42)
]
end
end
a_hash_includingis an alias for the includematcher.
2 / 9
Aliases
RSpec 3 provides one or more aliases for all the built-in matchers.
consistent phrasing ("a_[type of object][verb]ing")
so they are easy to guess:
a_string_starting_withfor start_with
a_string_includinga_collection_includinga_hash_includingaliases
of include
see a list of them in this gist
easier to read when used in compound expresions or composed
matchers
and also more readable failure mssages.
RSpec 3 made it easy to define an alias for some built-in matcher or even your
custom matchers. Here is the bit of code to define the a_string_starting_with
alias of start_with:
RSpec::Matchers.alias_matcher:a_string_starting_with,:start_with
3 / 9
What are these composable matchers good for?
They will save you from this ...
describe"GET/api/areas/:area_id/pscs"do
context"whengivenvaliddata"do
it"returnsthePSCSforgivenareainJSON"do
get"/api/areas/#{area.id}/pscs",
{access_token:access_token_for(user),level_id:area.default_level.id},
{'Accept'=>Mime::JSON}
expect(response.status).tobe(200)
expect(response.content_type).tobe(Mime::JSON)
json_response=json(response.body)
expect(json_response[:latitude]).to eq(area.location.point.latitude.to_f)
expect(json_response[:longitude]).to eq(area.location.point.longitude.to_f)
#otherlongexpectshere
expect(level_node[:previous_level][:level_id]).toeq(area.parkings_levels.order_by_lev
expect(level_node[:image][:url]).to eq(area.level_image(area.default_leve
pscs_latitudes=json_response[:pscs].map{|e|e[:pscs][:latitude]}
expect(pscs_latitudes).toinclude(area.pscs_on_level(area.default_level.id).first.poin
end
end
end
4 / 9
The solution
is to use the matchmatcher, which became in rspec 3 a kind of black hole for any rspec
matcher.
describe"GET/api/areas/:area_id/pscs"do
context"whengivenvaliddata"do
it"returnsthePSCSforgivenareainJSON"do
get"/api/areas/#{area.id}/pscs",
{level_id:area.default_level.id},
{
'Authorization'=>"Bearer#{access_token_for(user)}",
'Accept'=>Mime::JSON
}
expect(response).tohave_status(200).and_content_type(Mime::JSON)
json_response=json(response.body)
expect(json_response).tomatch(pscs_list_composed_matcher(area:area))
expect(json_response[:pscs]).tocontain_latitude(area.pscs_on_level(area.default_level
end
end
end
5 / 9
The object passed to matchis more like a big hash containing any rspec
matchers as values for his keys:
modulePscsHelpers
defpscs_list_composed_matcher(area:,current_level:nil,is_favorite:false)
current_level=area.default_level
{
latitude:area.location.point.latitude.to_f,
longitude:area.location.point.longitude.to_f,
is_favorite:is_favorite,
zoomLevel:(a_value>0),
level:current_level_matcher(area,current_level),
pscs:an_instance_of(Array)
}
end
defcurrent_level_matcher(area,current_level)
{
level_id:current_level.id,
name:current_level.name,
default_level:level_matcher(area.default_level),
next_level: level_matcher(area.levels.first),
previous_level:level_matcher(area.levels.last),
image:level_image_matcher(area,current_level)
}
end
#reusable
deflevel_matcher(level)
#...
end
deflevel_image_matcher(area,current_level)
#... 6 / 9
2. Custom matchers
2.1 How to:
RSpec::Matchers.define:contain_latitudedo|expected|
latitudes=[]
matchdo|actual|
latitudes=actual.collect{|item|item[:pscs][:latitude]}
latitudes.find{|lat|lat.to_s==expected.to_s}
end
failure_messagedo|actual|
"expectedthatpscs_listwithlatitudesn #{latitudes}nwouldcontainthe'#{expec
end
end
#anduseitlikethis:
expect(json_response[:pscs]).tocontain_latitude(45.4545)
#orusingacompoundexpression
expect(json_response[:pscs])
.tocontain_latitude(45.4545)
.andcontain_longitude(25.90)
7 / 9
2.2 Chained matchers with fluent interface
When you want something more expressive then .andor .orfrom previous
example
modulePscsHelpers
#scopedmatcherswiththePscsHelpersmodule
extendRSpec::Matchers::DSL
matcher:contain_a_latitude_bigger_thando|first|
latitudes=[]
matchdo|actual|
latitudes=actual.collect{|item|item[:pscs][:latitude]}
bigger=latitudes.find{|lat|lat>expected}
smaller=latitudes.find{|lat|lat<second}
bigger&&smaller
end
chain:but_smaller_thando|second|
@second=second
end
end
end
#andthefancyexpectationusingit
expect(response).tocontain_a_latitude_bigger_than(43).but_smaller_than(47)
8 / 9
Resources
RSpec 3 - Composable Matchers
List of RSpec 3 Aliases gist
Define Matcher
9 / 9

RSpec matchers

  • 1.
  • 2.
  • 3.
    Aliases RSpec 3 providesone or more aliases for all the built-in matchers. consistent phrasing ("a_[type of object][verb]ing") so they are easy to guess: a_string_starting_withfor start_with a_string_includinga_collection_includinga_hash_includingaliases of include see a list of them in this gist easier to read when used in compound expresions or composed matchers and also more readable failure mssages. RSpec 3 made it easy to define an alias for some built-in matcher or even your custom matchers. Here is the bit of code to define the a_string_starting_with alias of start_with: RSpec::Matchers.alias_matcher:a_string_starting_with,:start_with 3 / 9
  • 4.
    What are thesecomposable matchers good for? They will save you from this ... describe"GET/api/areas/:area_id/pscs"do context"whengivenvaliddata"do it"returnsthePSCSforgivenareainJSON"do get"/api/areas/#{area.id}/pscs", {access_token:access_token_for(user),level_id:area.default_level.id}, {'Accept'=>Mime::JSON} expect(response.status).tobe(200) expect(response.content_type).tobe(Mime::JSON) json_response=json(response.body) expect(json_response[:latitude]).to eq(area.location.point.latitude.to_f) expect(json_response[:longitude]).to eq(area.location.point.longitude.to_f) #otherlongexpectshere expect(level_node[:previous_level][:level_id]).toeq(area.parkings_levels.order_by_lev expect(level_node[:image][:url]).to eq(area.level_image(area.default_leve pscs_latitudes=json_response[:pscs].map{|e|e[:pscs][:latitude]} expect(pscs_latitudes).toinclude(area.pscs_on_level(area.default_level.id).first.poin end end end 4 / 9
  • 5.
    The solution is touse the matchmatcher, which became in rspec 3 a kind of black hole for any rspec matcher. describe"GET/api/areas/:area_id/pscs"do context"whengivenvaliddata"do it"returnsthePSCSforgivenareainJSON"do get"/api/areas/#{area.id}/pscs", {level_id:area.default_level.id}, { 'Authorization'=>"Bearer#{access_token_for(user)}", 'Accept'=>Mime::JSON } expect(response).tohave_status(200).and_content_type(Mime::JSON) json_response=json(response.body) expect(json_response).tomatch(pscs_list_composed_matcher(area:area)) expect(json_response[:pscs]).tocontain_latitude(area.pscs_on_level(area.default_level end end end 5 / 9
  • 6.
    The object passedto matchis more like a big hash containing any rspec matchers as values for his keys: modulePscsHelpers defpscs_list_composed_matcher(area:,current_level:nil,is_favorite:false) current_level=area.default_level { latitude:area.location.point.latitude.to_f, longitude:area.location.point.longitude.to_f, is_favorite:is_favorite, zoomLevel:(a_value>0), level:current_level_matcher(area,current_level), pscs:an_instance_of(Array) } end defcurrent_level_matcher(area,current_level) { level_id:current_level.id, name:current_level.name, default_level:level_matcher(area.default_level), next_level: level_matcher(area.levels.first), previous_level:level_matcher(area.levels.last), image:level_image_matcher(area,current_level) } end #reusable deflevel_matcher(level) #... end deflevel_image_matcher(area,current_level) #... 6 / 9
  • 7.
    2. Custom matchers 2.1How to: RSpec::Matchers.define:contain_latitudedo|expected| latitudes=[] matchdo|actual| latitudes=actual.collect{|item|item[:pscs][:latitude]} latitudes.find{|lat|lat.to_s==expected.to_s} end failure_messagedo|actual| "expectedthatpscs_listwithlatitudesn #{latitudes}nwouldcontainthe'#{expec end end #anduseitlikethis: expect(json_response[:pscs]).tocontain_latitude(45.4545) #orusingacompoundexpression expect(json_response[:pscs]) .tocontain_latitude(45.4545) .andcontain_longitude(25.90) 7 / 9
  • 8.
    2.2 Chained matcherswith fluent interface When you want something more expressive then .andor .orfrom previous example modulePscsHelpers #scopedmatcherswiththePscsHelpersmodule extendRSpec::Matchers::DSL matcher:contain_a_latitude_bigger_thando|first| latitudes=[] matchdo|actual| latitudes=actual.collect{|item|item[:pscs][:latitude]} bigger=latitudes.find{|lat|lat>expected} smaller=latitudes.find{|lat|lat<second} bigger&&smaller end chain:but_smaller_thando|second| @second=second end end end #andthefancyexpectationusingit expect(response).tocontain_a_latitude_bigger_than(43).but_smaller_than(47) 8 / 9
  • 9.
    Resources RSpec 3 -Composable Matchers List of RSpec 3 Aliases gist Define Matcher 9 / 9