9
rspec matchers Zaharie Marius - 06-03-2015 1 / 9

RSpec matchers

  • Upload
    rupicon

  • View
    62

  • Download
    0

Embed Size (px)

Citation preview

rspec matchersZaharie Marius - 06-03-2015

1 / 9

1. Composable matchers

Example:

# background_worker_spec.rbdescribe BackgroundWorker do it 'puts enqueued jobs onto the queue in order' do worker = BackgroundWorker.new worker.enqueue(:klass => "Class1", :id => 37) worker.enqueue(:klass => "Class2", :id => 42)

expect(worker.queue).to match [ a_hash_including(:klass => "Class1", :id => 37), a_hash_including(:klass => "Class2", :id => 42) ] endend

a_hash_including is an alias for the include matcher.

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_with for start_witha_string_including a_collection_including a_hash_including aliasesof includesee a list of them in this gist

easier to read when used in compound expresions or composedmatchersand also more readable failure mssages.

RSpec 3 made it easy to define an alias for some built-in matcher or even yourcustom matchers. Here is the bit of code to define the a_string_starting_withalias 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 "when given valid data" do it "returns the PSCS for given area in JSON" 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).to be(200) expect(response.content_type).to be(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)

# other long expects here

expect(level_node[:previous_level][:level_id]).to eq(area.parkings_levels.order_by_level.last.level.id) expect(level_node[:image][:url]).to eq(area.level_image(area.default_level.id).try(

pscs_latitudes = json_response[:pscs].map { |e| e[:pscs][:latitude] } expect(pscs_latitudes).to include(area.pscs_on_level(area.default_level.id).first.point.latitude.to_f) end endend

4 / 9

The solution

is to use the match matcher, which became in rspec 3 a kind of black hole for any rspecmatcher.

describe "GET /api/areas/:area_id/pscs" do context "when given valid data" do it "returns the PSCS for given area in JSON" do get "/api/areas/#{area.id}/pscs", { level_id: area.default_level.id }, { 'Authorization' => "Bearer #{access_token_for(user)}", 'Accept' => Mime::JSON }

expect(response).to have_status(200).and_content_type(Mime::JSON)

json_response = json(response.body)

expect(json_response).to match(pscs_list_composed_matcher(area: area))

expect(json_response[:pscs]).to contain_latitude(area.pscs_on_level(area.default_level.id).first.point.latitude.to_f) end endend

5 / 9

The object passed to match is more like a big hash containing any rspecmatchers as values for his keys:

module PscsHelpers def pscs_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

def current_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 def level_matcher(level) # ... end

def level_image_matcher(area, current_level) # ... 6 / 9

2. Custom matchers

2.1 How to:

RSpec::Matchers.define :contain_latitude do |expected| latitudes = [] match do |actual| latitudes = actual.collect { |item| item[:pscs][:latitude] }

latitudes.find { |lat| lat.to_s == expected.to_s } end

failure_message do |actual| "expected that pscs_list with latitudes \n #{latitudes} \n would contain the '#{expected} end end

# and use it like this:

expect(json_response[:pscs]).to contain_latitude(45.4545)

# or using a compound expression

expect(json_response[:pscs]) .to contain_latitude(45.4545) .and contain_longitude(25.90)

7 / 9

2.2 Chained matchers with fluent interface

When you want something more expressive then .and or .or from previousexample

module PscsHelpers # scoped matchers with the PscsHelpers module extend RSpec::Matchers::DSL

matcher :contain_a_latitude_bigger_than do |first| latitudes = [] match do |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_than do |second| @second = second end endend

# and the fancy expectation using it expect(response).to contain_a_latitude_bigger_than(43).but_smaller_than(47)

8 / 9