I have an array of hashes and I'm trying to assert that the array has exactly a certain number of hashes in a certain order that have a certain key.
So let's say I have an array of fruits.
fruits = [
{ name: 'apple', count: 3 },
{ name: 'orange', count: 14 },
{ name: 'strawberry', count: 7 },
]
When I use the eq
matcher with hash_including
(or include
which is its alias), the assertion fails.
# fails :(
expect(fruits).to eq([
hash_including(name: 'apple'),
hash_including(name: 'orange'),
hash_including(name: 'strawberry'),
])
It's weird that this doesn't work and I've always found a way around it and moved on, but it's been bothering me for a while, so I decided to post about it this time.
What I'm not looking for
Obviously this works but I like the other syntax because that's kinda the point of these matchers: so I don't have to transform my data structures by hand and have more readable specs.
fruit_names = fruits.map { |h| h.fetch(:name) }
expect(fruit_names).to eq(['apple', 'orange', 'strawberry'])
contain_exactly
and include
work but I care about the exact size of the array and the order of elements, which they fail to assert.
# passes but doesn't assert the size of the array or the order of elements
expect(fruits).include(
hash_including(name: 'apple'),
hash_including(name: 'orange'),
hash_including(name: 'strawberry'),
)
# passes but doesn't assert the exact order of elements
expect(fruits).contain_exactly(
hash_including(name: 'apple'),
hash_including(name: 'orange'),
hash_including(name: 'strawberry'),
)
CodePudding user response:
Looks like you just need to use match
fruits = [
{ name: 'apple', count: 3 },
{ name: 'orange', count: 14 },
{ name: 'strawberry', count: 7 },
]
expect(fruits).to match([
include(name: 'apple'),
include(name: 'orange'),
include(name: 'strawberry'),
])
This test will fail if some array element is missing or extra
This test will fail if some of hashes doesn't include specified key-value pair
This test will fail in case of wrong array elements order
CodePudding user response:
What you're doing is a bit of an anti-pattern. You should almost never write tests with that level of complexity, or bundle multiple tests into a single expectation. Doing so gives you poor granularity on results, and makes it harder for tests to identify problems.
You're much better off having different specs for each result, or simply looking for expected data or a full data structure of the structure is invariant. For example:
describe "#fruits" do
let(:fruits) do
[
{ name: 'apple', count: 3 },
{ name: 'orange', count: 14 },
{ name: 'strawberry', count: 7 },
]
end
it "should contain an apple" do
expect(fruits.keys).to inlcude("apple")
end
it "should contain an orange" do
expect(fruits.keys).to inlcude("orange")
end
it "should contain an apple" do
expect(fruits.keys).to inlcude("strawberry")
end
end
Otherwise, if you want the whole structure, just make sure it's ordered. Arrays are ordered, and need to be ordered to be equal. Hashes guarantee insert order, but are equal regardless of order if they contain the same content, so you'd want to compare:
# under your "#fruits" tests...
describe "sorted fruits" do
let(:sorted_fruits) { fruits.sort_by { _1[:name] } }
it "should have the same structure as the original array of hashes" do
expect(fruits).to eql(sorted_fruits)
end
it "should contain an apple as the first fruit" do
expect(sorted_fruits.first).to eql("apple")
end
end
There are other ways to do this, of course, but the point is that you want your tests to be DAMP, not DRY, and to keep your tests as free of new logic as possible. Otherwise, you're likely to introduce complexity to your tests that is exercising untested testing logic rather than simply testing the object under test. YMMV.
CodePudding user response:
I don't remember any built-in matcher that allows checking of inclusion and order at the same time (UPD. I'm wrong) while being flexible in terms of matching values, but if you need these checks often you could create a simple custom matcher quite easily.
The very basic version is quite straightforward:
RSpec::Matchers.define :contain_exactly_ordered do |expected_collection|
match do |actual|
expected_collection.each_with_index.all? do |expected_element, index|
values_match?(expected_element, actual[index])
end
end
end
and then
specify do
fruits = [
{ name: 'apple', count: 3 },
{ name: 'orange', count: 14 },
{ name: 'strawberry', count: 7 },
]
expect(fruits).to contain_exactly_ordered([
hash_including(name: 'apple'),
hash_including(name: 'orange'),
hash_including(name: 'strawberry'),
]) #=> This should be green
end
There is one serious drawback though - in case of the wrong order this matcher doesn't provide enough information to fix the error easily (it doesn't say which elements exactly are mismatched - not a big deal for 2-3 elements but for bigger collections, this might be a pain). So, ideally, we need to tailor the failure message to make it really helpful. Something like the following:
RSpec::Matchers.define :contain_exactly_ordered do |expected_collection|
match do |actual|
@mismatched_items = expected_collection.each_with_index.reject do |expected_element, index|
values_match?(expected_element, actual[index])
end
@mismatched_items.none?
end
failure_message do
@mismatched_items.map do |expectation, i|
" [#{i}]: expected #{expectation.description} to match #{actual[i].inspect}"
end.unshift("Mismatched items found at the following indices:").join("\n")
end
end
Now if we mess up the order:
specify do
fruits = [
{ name: 'apple', count: 3 },
{ name: 'orange', count: 14 },
{ name: 'strawberry', count: 7 },
]
expect(fruits).to contain_exactly_ordered([
hash_including(name: 'orange'),
hash_including(name: 'apple'),
hash_including(name: 'strawberry'),
])
end
our error message is quite helpful:
1) ... should contain exactly ordered hash_including(:name=>"orange"), hash_including(:name=>"apple"), and hash_including(:name=>"strawberry")
Failure/Error:
expect(fruits).to contain_exactly_ordered([
hash_including(name: 'orange'),
hash_including(name: 'apple'),
hash_including(name: 'strawberry'),
])
Mismatched items found at the following indices:
[0]: expected hash_including(:name=>"orange") to match {:name=>"apple", :count=>3}
[1]: expected hash_including(:name=>"apple") to match {:name=>"orange", :count=>14}