Permalink
Browse files

Merge pull request #571 from yelled3/add_have_attributes_matcher

New matcher: `have_attributes`
  • Loading branch information...
myronmarston committed Jun 24, 2014
2 parents ac3810d + 70794d2 commit 6f975b08c996b1014654334229d5d4b020055690
@@ -35,6 +35,15 @@ Bug Fixes:
expecatations) weren't being used. (Myron Marston, #566)
* Structs are no longer treated as arrays when diffed. (Jon Rowe, #576)
Enhancements:
* Add `have_attributes` matcher, that passes if actual's attribute
values match the expected attributes hash:
`Person = Struct.new(:name, :age)`
`person = Person.new("Bob", 32)`
`expect(person).to have_attributes(:name => "Bob", :age => 32)`.
(Adam Farhi)
### 3.0.0 / 2014-06-01
[Full Changelog](http://github.com/rspec/rspec-expectations/compare/v3.0.0.rc1...v3.0.0)
@@ -0,0 +1,47 @@
Feature: have_attributes matcher
Use the have_attributes matcher to specify that
an object's attributes match the expected attributes:
```ruby
Person = Struct.new(:name, :age)
person = Person.new("Jim", 32)
expect(person).to have_attributes(:name => "Jim", :age => 32)
expect(person).to have_attributes(:name => a_string_starting_with("J"), :age => (a_value > 30) )
```
The matcher will fail if actual doesn't respond to any of the expected attributes:
```ruby
expect(person).to have_attributes(:name => "Jim", :color => 'red')
```
Scenario: basic usage
Given a file named "basic_have_attributes_matcher_spec.rb" with:
"""ruby
Person = Struct.new(:name, :age)
RSpec.describe Person.new("Jim", 32) do
it { is_expected.to have_attributes(:name => "Jim") }
it { is_expected.to have_attributes(:name => a_string_starting_with("J") ) }
it { is_expected.to have_attributes(:age => 32) }
it { is_expected.to have_attributes(:age => (a_value > 30) ) }
it { is_expected.to have_attributes(:name => "Jim", :age => 32) }
it { is_expected.to have_attributes(:name => a_string_starting_with("J"), :age => (a_value > 30) ) }
it { is_expected.not_to have_attributes(:name => "Bob") }
it { is_expected.not_to have_attributes(:age => 10) }
it { is_expected.not_to have_attributes(:age => (a_value < 30) ) }
# deliberate failures
it { is_expected.to have_attributes(:name => "Bob") }
it { is_expected.to have_attributes(:age => 10) }
# fails if any of the attributes don't match
it { is_expected.to have_attributes(:name => "Bob", :age => 32) }
it { is_expected.to have_attributes(:name => "Jim", :age => 10) }
it { is_expected.to have_attributes(:name => "Bob", :age => 10) }
end
"""
When I run `rspec basic_have_attributes_matcher_spec.rb`
Then the output should contain "14 examples, 5 failures"
@@ -551,6 +551,30 @@ def exist(*args)
alias_matcher :an_object_existing, :exist
alias_matcher :existing, :exist
# Passes if actual's attribute values match the expected attributes hash.
# This works no matter how you define your attribute readers.
#
# @example
#
# Person = Struct.new(:name, :age)
# person = Person.new("Bob", 32)
#
# expect(person).to have_attributes(:name => "Bob", :age => 32)
# expect(person).to have_attributes(:name => a_string_starting_with("B"), :age => (a_value > 30) )
#
# @note It will fail if actual doesn't respond to any of the expected attributes.
#
# @example
#
# expect(person).to have_attributes(:color => "red")
#
# rubocop:disable Style/PredicateName
def have_attributes(expected)
BuiltIn::HaveAttributes.new(expected)
end
# rubocop:enable Style/PredicateName
alias_matcher :an_object_having_attributes, :have_attributes
# Passes if actual includes expected. This works for
# collections and Strings. You can also pass in multiple args
# and it will only pass if all args are found in collection.
@@ -30,6 +30,7 @@ module BuiltIn
autoload :Equal, 'rspec/matchers/built_in/equal'
autoload :Exist, 'rspec/matchers/built_in/exist'
autoload :Has, 'rspec/matchers/built_in/has'
autoload :HaveAttributes, 'rspec/matchers/built_in/have_attributes'
autoload :Include, 'rspec/matchers/built_in/include'
autoload :All, 'rspec/matchers/built_in/all'
autoload :Match, 'rspec/matchers/built_in/match'
@@ -0,0 +1,84 @@
module RSpec
module Matchers
module BuiltIn
# @api private
# Provides the implementation for `have_attributes`.
# Not intended to be instantiated directly.
class HaveAttributes < BaseMatcher
# @private
attr_reader :respond_to_failed
def initialize(expected)
@expected = expected
@respond_to_failed = false
end
# @api private
# @return [Boolean]
def matches?(actual)
@actual = actual
return false unless respond_to_attributes?
perform_match(:all?)
end
# @api private
# @return [Boolean]
def does_not_match?(actual)
@actual = actual
return false unless respond_to_attributes?
perform_match(:none?)
end
# @api private
# @return [String]
def description
described_items = surface_descriptions_in(expected)
improve_hash_formatting "have attributes #{described_items.inspect}"
end
# @api private
# @return [String]
def failure_message
respond_to_failure_message_or { super }
end
# @api private
# @return [String]
def failure_message_when_negated
respond_to_failure_message_or { super }
end
private
def perform_match(predicate)
expected.__send__(predicate) do |attribute_key, attribute_value|
actual_has_attribute?(attribute_key, attribute_value)
end
end
def actual_has_attribute?(attribute_key, attribute_value)
actual_value = actual.__send__(attribute_key)
values_match?(attribute_value, actual_value)
end
def respond_to_attributes?
matches = respond_to_matcher.matches?(actual)
@respond_to_failed = !matches
matches
end
def respond_to_matcher
@respond_to_matcher ||= RespondTo.new(*expected.keys).with(0).arguments
end
def respond_to_failure_message_or
if respond_to_failed
respond_to_matcher.failure_message
else
improve_hash_formatting(yield)
end
end
end
end
end
end
@@ -212,6 +212,14 @@ module RSpec
expect(existing).to be_aliased_to(exist).with_description("existing")
end
specify do
expect(
an_object_having_attributes(:age => 32)
).to be_aliased_to(
have_attributes(:age => 32)
).with_description("an object having attributes {:age => 32}")
end
specify do
expect(
a_string_including("a")
@@ -0,0 +1,177 @@
RSpec.describe "#have_attributes matcher" do
Person = Struct.new(:name, :age)
class Person
def parent(parent_name)
@parent = parent_name
end
end
let(:wrong_name) { "Wrong Name" }
let(:wrong_age) { 11 }
let(:correct_name) { "Correct name" }
let(:correct_age) { 33 }
let(:person) { Person.new(correct_name, correct_age) }
it "is not diffable" do
expect(have_attributes(:age => correct_age)).to_not be_diffable
end
describe "expect(...).to have_attributes(with_one_attribute)" do
it_behaves_like "an RSpec matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do
let(:matcher) { have_attributes(:name => "Correct name") }
end
it "passes if target has the provided attributes" do
expect(person).to have_attributes(:name => correct_name)
end
it "fails if target does not have any of the expected attributes" do
expect {
expect(person).to have_attributes(:name => wrong_name)
}.to fail_matching(%r|expected #{object_inspect person} to have attributes #{hash_inspect :name => wrong_name}|)
end
it "fails if target does not responds to any of the attributes" do
expect {
expect(person).to have_attributes(:color => 'red')
}.to fail_matching("expected #{object_inspect person} to respond to :color")
end
it "fails if target responds to the attribute but requires arguments" do
expect {
expect(person).to have_attributes(:parent => 'Billy')
}.to fail_matching("expected #{object_inspect person} to respond to :parent with 0 arguments")
end
describe "expect(...).to have_attributes(key => matcher)" do
it "passes when the matchers match" do
expect(person).to have_attributes(:age => (a_value > 30))
end
it 'provides a description' do
description = have_attributes(:age => (a_value > 30)).description
expect(description).to eq("have attributes {:age => (a value > 30)}")
end
it "fails with a clear message when the matcher does not match" do
expect {
expect(person).to have_attributes(:age => (a_value < 10))
}.to fail_matching("expected #{object_inspect person} to have attributes {:age => (a value < 10)}")
end
end
end
describe "expect(...).to_not have_attributes(with_one_attribute)" do
it "passes if target does not have any of the expected attributes" do
expect(person).to_not have_attributes(:age => wrong_age)
end
it "fails if target has all of the expected attributes" do
expect {
expect(person).to_not have_attributes(:age => correct_age)
}.to fail_matching(%r|expected #{object_inspect person} not to have attributes #{hash_inspect :age => correct_age}|)
end
it "fails if target does not responds to any of the attributes" do
expect {
expect(person).to_not have_attributes(:color => 'red')
}.to fail_matching("expected #{object_inspect person} to respond to :color")
end
it "fails if target responds to the attribute but requires arguments" do
expect {
expect(person).to_not have_attributes(:parent => 'Billy')
}.to fail_matching("expected #{object_inspect person} to respond to :parent with 0 arguments")
end
end
describe "expect(...).to have_attributes(with_multiple_attributes)" do
it_behaves_like "an RSpec matcher", :valid_value => Person.new("Correct name", 33), :invalid_value => Person.new("Wrong Name", 11) do
let(:matcher) { have_attributes(:name => "Correct name", :age => 33) }
end
it "passes if target has the provided attributes" do
expect(person).to have_attributes(:name => correct_name, :age => correct_age)
end
it "fails if target does not have any of the expected attributes" do
expect {
expect(person).to have_attributes(:name => correct_name, :age => wrong_age)
}.to fail_matching(%r|expected #{object_inspect person} to have attributes #{hash_inspect :name => correct_name, :age => wrong_age}|)
end
it "fails if target does not responds to any of the attributes" do
expect {
expect(person).to have_attributes(:name => correct_name, :color => 'red')
}.to fail_matching("expected #{object_inspect person} to respond to :color")
end
it "fails if target responds to the attribute but requires arguments" do
expect {
expect(person).to have_attributes(:name => correct_name, :parent => 'Billy')
}.to fail_matching("expected #{object_inspect person} to respond to :parent with 0 arguments")
end
end
describe "expect(...).to_not have_attributes(with_multiple_attributes)" do
it "passes if target has none of the expected attributes" do
expect(person).to_not have_attributes(:name => wrong_name, :age => wrong_age)
end
it "fails if target has any of the expected attributes" do
expect {
expect(person).to_not have_attributes(:name => wrong_name, :age => correct_age)
}.to fail_matching(%r|expected #{object_inspect person} not to have attributes #{hash_inspect :name => wrong_name, :age => correct_age}|)
end
it "fails if target has all of the expected attributes" do
expect {
expect(person).to_not have_attributes(:name => correct_name, :age => correct_age)
}.to fail_matching(%r|expected #{object_inspect person} not to have attributes #{hash_inspect :name => correct_name, :age => correct_age}|)
end
it "fails if target does not responds to any of the attributes" do
expect {
expect(person).to_not have_attributes(:name => correct_name, :color => 'red')
}.to fail_matching("expected #{object_inspect person} to respond to :color")
end
it "fails if target responds to the attribute but requires arguments" do
expect {
expect(person).to_not have_attributes(:name => correct_name, :parent => 'Billy')
}.to fail_matching("expected #{object_inspect person} to respond to :parent with 0 arguments")
end
end
include RSpec::Matchers::Pretty
# We have to use Hash#inspect in examples that have multi-entry
# hashes because the #inspect output on 1.8.7 is non-deterministic
# due to the fact that hashes are not ordered. So we can't simply
# put a literal string for what we expect because it varies.
if RUBY_VERSION.to_f == 1.8
def hash_inspect(hash)
/\{(#{hash.map { |key,value| "#{key.inspect} => #{value.inspect}.*" }.join "|" }){#{hash.size}}\}/
end
else
def hash_inspect(hash)
improve_hash_formatting hash.inspect
end
end
include RSpec::Matchers::Composable
# a helper for failure message assertion
def object_inspect(object)
surface_descriptions_in object.inspect
end
end

0 comments on commit 6f975b0

Please sign in to comment.