rspec - matcher for one of choices - ruby

I have method returning random value from the predefined array (namely: [ 'value1', 'value2']).
How should I test that with rspec?
I'd like to do something like:
expect(FooClass.new.value).to be_in ['value1', 'value2']
Any way to do that? Thanks.

Use this
expect(['value1', 'value2']).to include(FooClass.new.value)
Or a simple Boolean match
expect(['value1', 'value2'].include? FooClass.new.value).to be true

Also, there is an or
expect('value').to eq('value1').or eq('value2')
Advantages:
It sounds like normal English.
The error message is a bit longer but contains all relevant info:
expected: "value1"
got: "value"
...or:
expected: "value2"
got: "value"
As muirbot pointed out you should pass the value you are testing to expect(), not the other way.
It's more flexible, it will work if someone came here looking for a solution to something like:
expect({one: 1, two: 2, three: 3}).to have_key(:one).or have_key(:first)

This does work, but is unconventional because the value you pass to expect should be the value you're testing.
expect(['value1', 'value2']).to include(FooClass.new.value)
I think it would be better to do
expect(Foo.new.value).to satisfy { |value| ['value1', 'value2'].include?(value) }
This will also give you a more accurate message when your test fails.

If you need this behavior often, you can write your own matcher. Here is the one I wrote - you can stick this in your spec file directly or into any file included by your test suite:
RSpec::Matchers.define(:be_one_of) do |expected|
match do |actual|
expected.include?(actual)
end
failure_message do |actual|
"expected one of #{expected}, got #{actual}"
end
end
This has a nicer failure message than any of the other answers so far (in my opinion). For instance:
Failures:
1) mydata is either empty or a list
Failure/Error: expect(mydata.class).to(be_one_of([NilClass, Array]))
expected one of [NilClass, Array], got String
Or you can customize the error message if some other wording makes more sense to you.

Related

Ruby passing symbol(s) as keyword argument(s)?

I have a method, where I want to use keyword parameter(s) and case/when statements, where I prefered to use symbols:
def navigate_to(page:)
case page
when :inbox
# some code
when :archive
# some code
else
# some code
end
end
Is this the correct syntax when calling the method?
navigate_to page: :inbox
The : : seems a little bit strange.
The keyword argument syntax is:
key: value
If key is page and value is the symbol :inbox, you indeed have:
page: :inbox
You could also use a positional argument instead:
def navigate_to(page)
# same as your code
end
navigate_to :inbox
Yes, the correct syntax is navigate_to page: :inbox.
While this is common Ruby, it is short and equivalent for several different things. First, the braces.
You are actually calling:
navigate_to(page: :inbox)
Second, the keyword argument pattern originates from hashes as arguments. Before there were keyword arguments, a common way would be to pass in a hash[1], like so:
def navigate_to(options)
page = options[:page]
end
navigate_to({ page: :inbox })
But when last argument in a method call is a hash, one can leave out the {}.
And last, the actual keys in the hash. A while ago (1.8 -> 1.9 IIRC) a short version was introduced for the following:
{ :page => 'Some Value' }, namely { page: 'Some Value' }. When Some Value is a symbol, that becomes { page: :inbox }.
So, taking all that:
navigate_to page: :inbox
Orignates from:
navigate_to({ :page => :inbox })
It might make more sense reading it like this, or knowing it comes from that.
And I know Ruby, nor ruby-ist, like braces, (), as can be seen in the mindboggling DSL of for example rspec, but I can advise especially new developers, to add them. It often makes code better understandable.
navigate_to(page: :inbox) is probably easier to understand than navigate_to page: :inbox, especially when you start calling through other methods: navigate_to page page_from_session :user.
[1] But, to stress, that is not really what is happening here. Keyword arguments and hash arguments do differ, now that we have keyword arguments. this just shows why the syntax is this way.

Confusing RSpec hash match diff

I'm using RSpec match matcher to check if a hash contains expected values. When a key of the hash doesn't match, all the dynamic (a_string_starting_with, etc) values are shown as not matching. It's especially distracting when you try to match a bigger hash. I'm wondering if there's another way check the hash, so only the values which really do not match would show up in the diff.
Here's an example, where a is marked in red, although the value is correct.
it 'matches' do
actual = {
a: 'test test',
b: 1,
c: 2,
}
expect(actual).to match(
a: a_string_starting_with('test'),
b: 0,
c: 2,
)
end
I'm wondering if there's another matcher I should use. Or if there are any custom matchers or gems for this?
The problem with this is the current differ gem used by RSpec and they are already aware of the issue, though currently no fix exists, as can be seen by these tickets:
https://github.com/rspec/rspec-support/issues/365
https://github.com/rspec/rspec-expectations/issues/1120
One of the solutions in proposed for now in the ticket is similar to what Mosaaleb is suggesting.
I got the same problem, this works to me:
# spec/supports/hash_diff_patcher.rb
module HashDiffPatcher
def diff_as_object(actual, expected)
if kind_of_hash?(actual) && kind_of_hash?(expected)
super(actual.sort.to_h, expected.sort.to_h)
else
super
end
end
private
def kind_of_hash?(obj)
# compact grape entity
obj.instance_of?(Hash) || obj.instance_of?(Grape::Entity::Exposure::NestingExposure::OutputBuilder)
end
end
RSpec::Support::Differ.prepend HashDiffPatcher

Expect assertion that is only true for one element in an array [duplicate]

This question already has answers here:
RSpec matcher that checks collection to include item that satisfies lambda
(2 answers)
Closed 3 years ago.
I want to assert that an array contains at least one element that passes an RSpec expectation. But most of the elements in the array will not pass the expectation. So I'd like to do something like this:
it "finds one element that matches" do
array.any? do |element|
expect(element).to eq("expected value")
end
end
And have the test pass if any of the elements pass the expectation. But, of course, the test will fail as I've written it here.
Is there a pattern in RSpec to do what I want to accomplish?
I don't want to do this:
it "finds one element that matches" do
expect(array.any? {|val| val == "expected value"}).to be_true
end
Because it's not clear to me how to manually check the same thing as the matcher I need to use in my test. I want to use the have_attributes matcher, which does some subtle metaprogramming magic I don't want to risk messing up trying to re-implement on my own.
You can use the include matcher to compose matchers:
expect(array).to include(a_string_matching(/foo/))
Despite the somewhat awkward syntax, you can use this with have_attributes:
expect(obj).to have_attributes(tags: include(a_string_matching(/foo/))
But if that's not flexible enough for whatever reason, you can use the satisfy matcher:
expect(array).to satisfy {|arr| arr.any? {|val| val == "expected value"})
The double-nested block is somewhat awkward by itself, but the flexibility of satisfy lets you do all kinds of stuff with it, and you can use the include matcher to get you there. For example:
require "rspec"
require "ostruct"
obj = OpenStruct.new(name: "foobar", tags: %w(bin bazzle))
describe obj do
it "has a bin tag" do
is_expected.to have_attributes(tags: include(/bin/))
end
it "has a tag 3 characters long" do
is_expected.to have_attributes(tags: include(satisfy { |t| t.length == 3 }))
end
end
If you're willing to add a gem, I really like rspec-its for cases like these: they can clean up the specs for individual attributes of an object which don't warrant their own subject block nicely:
describe obj do
its(:tags) { is_expected.to be_a Array }
its(:tags) { is_expected.to include "bin" }
end
RSpec has composing matchers that can be used with expect(array).to include to achieve what I wanted. For example:
expect(array).to include(a_string_matching("expected value"))
For the have_attributes matcher specifically, RSpec has an alias for it called an_object_having_attributes, allowing me to write:
expect(array).to include(an_object_matching(object_i_want_duplicated))

RSpec magic for testing an array's none? method?

I have an Array/Enumerable where each entity is a Hash. Using RSpec what is the most idiomatic way to test for "none of the entries in the enumerable should have a key called 'body'"?
I can do something like:
array.none? {|thing| thing.key? 'body'}.should be_true
or
array.should be_none {|thing| thing.key? 'body'}
...but there must be a more RSpec-way of doing this, correct?
I can't seem to find an appropriate built-in matcher. Is the answer a custom matcher?
I would use
responses.should be_none { |response| response.key? 'body' }
Between the two you gave. This would be slightly more helpful with an error like
"Expected none? to return true"
where as your first example would say something like
"expected False:false class to be true"
The third option I can see would be something like
keys = responses.map { |response| response.keys }.flatten.uniq
keys.should_not include "body"
This would give an error like
expected ["foo", "bar", "body"] not to include "body"
Other than that, looking at https://www.relishapp.com/rspec/rspec-expectations/v/2-11/docs/built-in-matchers/satisfy-matcher
you could try
responses.should satisfy { |response| not response.keys.include? "body" }
Another option would be turning the be_none statement around with be_any:
responses.should_not be_any { |response| response.key? 'body' }
I would assume that the result is equivalent as any? is the negation of none?.
It's mostly a question of which option you feel is the most intuitive to read and, as EnabrenTane mentions, if you think the error messages are helpful enough.

is this a valid ruby syntax?

if step.include? "apples" or "banana" or "cheese"
say "yay"
end
Several issues with your code.
step.include? "apples" or "banana" or "cheese"
This expression evaluates to:
step.include?("apples") or ("banana") or ("cheese")
Because Ruby treats all values other than false and nil as true, this expression will always be true. (In this case, the value "banana" will short-circuit the expression and cause it to evaluate as true, even if the value of step does not contain any of these three.)
Your intent was:
step.include? "apples" or step.include? "banana" or step.include? "cheese"
However, this is inefficient. Also it uses or instead of ||, which has a different operator precedence, and usually shouldn't be used in if conditionals.
Normal or usage:
do_something or raise "Something went wrong."
A better way of writing this would have been:
step =~ /apples|banana|cheese/
This uses a regular expression, which you're going to use a lot in Ruby.
And finally, there is no say method in Ruby unless you define one. Normally you would print something by calling puts.
So the final code looks like:
if step =~ /apples|banana|cheese/
puts "yay"
end
The last two terms appear to Ruby as true, rather than having anything to do with the include? phrase.
Assuming that step is a string...
step = "some long string with cheese in the middle"
you could write something like this.
puts "yay" if step.match(/apples|banana|cheese/)
Here's a way to call step.include? on each of the arguments until one of them returns true:
if ["apples", "banana", "cheese"].any? {|x| step.include? x}
It's definitely not what you appear to be wanting. The include? method takes in a String, which is not what "apples" or "banana" or "cheese" produces. Try this instead:
puts "yay" if ["apples", "banana", "cheese"].include?(step)
But it's unclear from the context what step is supposed to be. If it's just the single word, then this is fine. If it can be a whole sentence, try joel.neely's answer.
The closest thing to that syntax that would do what you appear to want would be something like:
if ["apples", "banana", "cheese"].include?(step)
puts "yay"
end
But one of the other suggestions using a regex would be more concise and readable.
Assuming step is an Array or a Set or something else that supports set intersection with the & operator, I think the following code is the most idiomatic:
unless (step & ["apples","banana","cheese"]).empty?
puts 'yay'
end
I'll add some parentheses for you:
if (step.include? "apples") or ("banana") or ("cheese")
say "yay"
end
(That would be why it's always saying "yay" -- because the expression will always be true.)
Just to add another side to this...
If step is an Array (as calling include? seems to suggest) then maybe the code should be:
if (step - %w{apples banana cheese}) != step
puts 'yay'
end

Resources