I am trying to get a good output for a custom matcher I wrote:
RSpec::Matchers.define :scope_limited_to do |expected|
match do |actual|
#actual = actual.limit_value
expected == #actual
end
failure_message do |actual|
"expected scope to be limited to #{expected}, but was limited to #{actual} instead"
end
end
Things work perfectly when using regular expectation:
expect(scope).to scope_limited_to(10)
# output
Failure/Error: expect(scope).to scope_limited_to(10)
expected scope to be limited to 10, but was limited to 11 instead
But when I use this matcher as an argument matcher:
expect(PlayerCsv).to receive(:new).with(scope_limited_to(10))
# output
Failure/Error: PlayerCsv.new(players).perform
#<PlayerCsv (class)> received :new with unexpected arguments
expected: (scope limited to 10)
got: (#<ActiveRecord::Relation [#<Player id: 136, username: "user1", birthdate: "2001-05-13", email: "email..._id: nil, player_merged_to_id: nil, inventory_access_toggled: nil, seon_registration_session: nil>]>)
Is there any way to improve the "got" part of the spec output? Ideally I'd like to see "(scope limited to 11)".
Related
I'm writing an expectation which checks whether a method is called two times with different arguments and returns different values. At the moment I'm just writing the expectation twice:
expect(ctx[:helpers]).to receive(:sanitize_strip).with(
%{String\ndescription <br/>and newline\n<br>},
length: nil
).and_return('String description and newline')
expect(ctx[:helpers]).to receive(:sanitize_strip).with(
%{String\ndescription <br/>and newline\n<br>},
length: 15
).and_return('String descr...')
I wonder if I can use receive ... exactly ... with ... and_return ... instead; something like:
expect(ctx[:helpers]).to receive(:sanitize_strip).exactly(2).times.with(
%{String\ndescription <br/>and newline\n<br>},
length: nil
).with(
%{String\ndescription <br/>and newline\n<br>},
length: 15
).and_return('String descr...', 'String description and newline')
The code above doesn't work, it raises the following error:
1) Types::Collection fields succeeds
Failure/Error: context[:helpers].sanitize_strip(text, length: truncate_at)
#<Double :helpers> received :sanitize_strip with unexpected arguments
expected: ("String\ndescription <br/>and newline\n<br>", {:length=>15})
got: ("String\ndescription <br/>and newline\n<br>", {:length=>nil})
Diff:
## -1,3 +1,3 ##
["String\ndescription <br/>and newline\n<br>",
- {:length=>15}]
+ {:length=>nil}]
Is there a way to use receive ... exactly ... with ... and_return ... with different with arguments?
There's an rspec-any_of gem that allows for the following syntax by prodiding all_of argument matcher:
expect(ctx[:helpers]).to receive(:sanitize_strip).with(
%{String\ndescription <br/>and newline\n<br>}
all_of({length: 15}, {length: nil})
)
.and_return('String descr...', 'String description and newline')
.twice
Without using an extra gem, you could call the normal expect ... to receive ... with ... and_return ... with variables from inside an each block:
describe '#sanitize_strip' do
let(:html) do
%{String\ndescription <br/>and newline\n<br>}
end
let(:test_data) do
[
[[html, length: nil], 'String description and newline'],
[[html, length: 15], 'String descr...']
]
end
it 'returns sanitized strings stripped to the number of characters provided' do
test_data.each do |args, result|
expect(ctx[:helpers]).to receive(:sanitize_strip).with(*args).and_return(result)
end
# Trigger the calls to sanitize_strip
end
end
How do I make the bot reply even if user’s message doesn’t 100% match with words I wrote in ‘when’? I want to make the bot trigger if someone sent “hello there”, for example, instead of just hello.
I’ve heard about when include?('example') but I can’t make it work.
require 'telegram/bot'
token = 'x'
Telegram::Bot::Client.run(token) do |bot|
bot.listen do |message|
puts "##{message.from.username}: #{message.text}"
case message.text
when 'hello'
bot.api.send_message(chat_id: message.chat.id, text: "answer")
end
end
end
You can use a regexp to match your text:
case message.text
when /hello/ # matches if there is the 'hello' somewhere in the string
# ...
when /\Ahello/ # matches if the string starts with 'hello'
# ...
end
Or you can use include? in an if condition:
if message.text.include?('hello')
# ...
end
I have a custom matcher in RSpec, that ignores whitespaces / newlines, and just matches content:
RSpec::Matchers.define :be_matching_content do |expected|
match do |actual|
actual.gsub(/\s/,'').should == expected.gsub(/\s/,'')
end
diffable
end
I can use it like this:
body = " some data \n more data"
body.should be_matching_content("some data\nmore wrong data")
However, when a test fails (like the one above), the diff output looks not good:
-some data
-more wrong data
+ some data
+ more data
Is it possible to configure the diffable output? The first line some data is right, but the second more wrong data is wrong. It would be very useful, to only get the second line as the root cause of the failure.
I believe you should disable default diffable behaviour in RSpec and substitute your own implementation:
RSpec::Matchers.define :be_matching_content do |expected|
match do |actual|
#stripped_actual = actual.gsub(/\s/,'')
#stripped_expected = expected.gsub(/\s/,'')
expect(#stripped_actual).to eq #stripped_expected
end
failure_message do |actual|
message = "expected that #{#stripped_actual} would match #{#stripped_expected}"
message += "\nDiff:" + differ.diff_as_string(#stripped_actual, #stripped_expected)
message
end
def differ
RSpec::Support::Differ.new(
:object_preparer => lambda { |object| RSpec::Matchers::Composable.surface_descriptions_in(object) },
:color => RSpec::Matchers.configuration.color?
)
end
end
RSpec.describe 'something'do
it 'should diff correctly' do
body = " some data \n more data"
expect(body).to be_matching_content("some data\nmore wrong data")
end
end
produces the following:
Failures:
1) something should diff correctly
Failure/Error: expect(body).to be_matching_content("some data\nmore wrong data")
expected that somedatamoredata would match somedatamorewrongdata
Diff:
## -1,2 +1,2 ##
-somedatamorewrongdata
+somedatamoredata
You can use custom differ if you want, even reimplement this whole matcher to a system call to diff command, something like this:
♥ diff -uw --label expected --label actual <(echo " some data \n more data") <(echo "some data\nmore wrong data")
--- expected
+++ actual
## -1,2 +1,2 ##
some data
- more data
+more wrong data
Cheers!
You can override the expected and actual methods that will then be used when generating the diff. In this example, we store the expected and actual values as instance variables and define methods that return the instance variables:
RSpec::Matchers.define :be_matching_content do |expected_raw|
match do |actual_raw|
#actual = actual_raw.gsub(/\s/,'')
#expected = expected_raw.gsub(/\s/,'')
expect(expected).to eq(#actual)
end
diffable
attr_reader :actual, :expected
end
Another example is to match for specific attributes in two different types of objects. (The expected object in this case is a Client model.)
RSpec::Matchers.define :have_attributes_of_v1_client do |expected_client|
match do |actual_object|
#expected = client_attributes(expected_client)
#actual = actual_object.attributes
expect(actual_object).to have_attributes(#expected)
end
diffable
attr_reader :actual, :expected
def failure_message
"expected attributes of a V1 Client view row, but they do not match"
end
def client_attributes(client)
{
"id" => client.id,
"client_type" => client.client_type.name,
"username" => client.username,
"active" => client.active?,
}
end
end
Example failure looks like this:
Failure/Error: is_expected.to have_attributes_of_v1_client(client_active_partner)
expected attributes of a V1 Client view row, but they do not match
Diff:
## -1,6 +1,6 ##
"active" => true,
-"client_type" => #<ClientType id: 2, name: "ContentPartner">,
+"client_type" => "ContentPartner",
"id" => 11,
There is a gem called diffy which can be used.
But it goes through a string line by line and compares them so instead of removing all whitespace you could replace any amount of whitespace with a newline and diff those entries.
This is an example of something you could do to improve your diffs a little bit. I am not 100% certain about where to insert this into your code.
def compare(str1, str2)
str1 = break_string(str1)
str2 = break_string(str2)
return true if str1 == str2
puts Diffy::Diff.new(str1, str2).to_s
return false
end
def break_string(str)
str.gsub(/\s+/,"\n")
end
The diffy gem can be set to produce color output suitable for the terminal.
Using this code would work like this
str1 = 'extra some content'
str2 = 'extra more content'
puts compare(str1, str2)
this would print
extra
-some # red in terminal
+more # green in terminal
content
\ No newline at end of file
I'm trying to create helper to test JSON responses in uniform and nice way.
For more descriptive and specific failures I want to generate one example per JSON atom.
Example syntax:
RSpec.describe "some JSON API View" do
setup_objects
before { render }
describe "response" do
subject { rendered }
it_conforms_to_json(
id: 27,
email: "john.smith#example.com",
name: "John",
profile_description: %r{professional specialist},
nested: {
id: 37,
status: "rejected"
}
)
end
end
So this snippet will be an equivalent to:
RSpec.describe "some JSON API View" do
setup_objects
before { render }
describe "response" do
subject { rendered }
it 'has equal value at object["id"]' do
expect(subject["id"]).to eq(27)
end
it 'has equal value at object["email"]' do
expect(subject["email"]).to eq("john.smith#example.com")
end
it 'has equal value at object["name"]' do
expect(subject["name"]).to eq("John")
end
it 'has matching value at object["profile_description"]' do
expect(subject["profile_description"]).to match(%r{professional specialist})
end
it 'has equal value at object["nested"]["id"]' do
expect(subject["nested"]["id"]).to eq(37)
end
it 'has equal value at object["nested"]["status"]' do
expect(subject["nested"]["status"]).to eq("rejected")
end
end
end
I was able to achieve that easily with this snippet:
module JsonHelper
extend self
def it_conforms_to_json(json)
generate_examples_for(json)
end
private
def generate_examples_for(json, opts)
with_prefix = opts.fetch(:with_prefix, [])
if json.is_a?(Hash)
json.each do |key, new_json|
new_prefix = with_prefix + [key.to_s]
generate_examples_for(new_json, opts.merge(with_prefix: new_prefix))
end
elsif json.is_a?(Array)
raise NotImplemented.new("Arrays are not allowed yet")
elsif json.is_a?(String) || json.is_a?(Numeric)
it "is expected to have equal value at json[\"#{with_prefix.join('"]["')}\"]" do
value = JSON.parse(subject)
with_prefix.each { |key| value = value[key.to_s] }
expect(value).to eq(json)
end
end
end
end
And just enabling it by extending it: rspec_config.extend JsonHelper
Obvious problem starts when you think about pretty failure messages:
They show backtrace including location of it "is expected...bla-bla-bla
They exclude location of it_conforms_to_json(...) call
First is fixable by adding backtrace exclusion/clean pattern, but it results in a full backtrace because everything is filtered.
Second and previous is fixable by wrapping expect statement in begin..rescue, mangling with backtrace by prepending it with file_path:line of it_conforms_to_json(...) call and re-raising modified exception.
After first 2 problems are being resolved, the new one arises:
"Unable to find matching line from the backtrace"
Suspected method is find_first_non_rspec_line (or something similar), that finds first line in a backtrace of it call (not exception backtrace) by applying default rspec exclusion regex'es.
It is fixable by mangling with RSpec internals, i.e.:
For rspec 2: monkey patch method that applies this regex'es
For rspec 3: undefine constant LIB_REGEX and define it again adding this helper files to this regex
For rspec 3 (recent minor/patch versions): the same, but with IGNORE_REGEX
Code of this fixup becomes shitty and unreadable, and it will be hell to maintain, because it depends on rspec inner implementation, that can change from minor/patch version to version. Who interested in reading this ugly thing here
More that that, it acts differently for rspec 2 and rspec 3.
rspec 2, perfectly what is required:
Failures:
1) A json response is expected to have equal value at json["id"]
Failure/Error: it_conforms_to_json(
expected: 37
got: 25
(compared using ==)
# ./spec/simple_with_fail_spec.rb:10:in `block in <top (required)>'
2) A json response is expected to have equal value at json["name"]
Failure/Error: it_conforms_to_json(
expected: "Smith"
got: "John"
(compared using ==)
# ./spec/simple_with_fail_spec.rb:10:in `block in <top (required)>'
rspec 3, slightly off, but still acceptable:
Failures:
1) A json response is expected to have equal value at json["id"]
Failure/Error: expect(value).to eq(json)
expected: 37
got: 25
(compared using ==)
# ./spec/simple_with_fail_spec.rb:10:in `block in <top (required)>'
# ./lib/rspec/json_expectations/expectations.rb:32:in `block in generate_examples_for'
2) A json response is expected to have equal value at json["name"]
Failure/Error: expect(value).to eq(json)
expected: "Smith"
got: "John"
(compared using ==)
# ./spec/simple_with_fail_spec.rb:10:in `block in <top (required)>'
# ./lib/rspec/json_expectations/expectations.rb:32:in `block in generate_examples_for'
So here are the questions:
Is there any built-in public-API means of achieving it?
It seems I have problems with naming here, it is not expectations, but what...?
So, it is a bit ugly now (first iteration, though), but works. No mangling with RSpec internals, using only public RSpec API, with nice error messages, without even cleaning a backtrace:
module RSpec
module JsonExpectations
class JsonTraverser
def self.traverse(errors, expected, actual, prefix=[])
if expected.is_a?(Hash)
expected.map do |key, value|
new_prefix = prefix + [key]
if actual.has_key?("#{key}")
traverse(errors, value, actual["#{key}"], new_prefix)
else
errors[new_prefix.join("/")] = :no_key
false
end
end.all?
elsif expected.is_a?(String) || expected.is_a?(Numeric)
if actual == expected
true
else
errors[prefix.join("/")] = {
actual: actual,
expected: expected
}
false
end
else
raise NotImplementedError, "#{expected} expectation is not supported"
end
end
end
end
end
RSpec::Matchers.define :include_json do |expected|
match do |actual|
unless expected.is_a?(Hash)
raise ArgumentError, "Expected value must be a json for include_json matcher"
end
RSpec::JsonExpectations::JsonTraverser.traverse(
#include_json_errors = {},
expected,
JSON.parse(actual)
)
end
# RSpec 2 vs 3
send(respond_to?(:failure_message) ?
:failure_message :
:failure_message_for_should) do |actual|
res = []
#include_json_errors.each do |json_path, error|
res << %{
json atom on path "#{json_path}" is missing
} if error == :no_key
res << %{
json atom on path "#{json_path}" is not equal to expected value:
expected: #{error[:expected].inspect}
got: #{error[:actual].inspect}
} if error.is_a?(Hash) && error.has_key?(:expected)
end
res.join("")
end
end
It allows this syntax now:
it "has basic info about user" do
expect(subject).to include_json(
id: 37,
email: "john.smith#example.com",
name: "Smith"
)
end
It generates errors like this:
F
Failures:
1) A json response has basic info about user
Failure/Error: expect(subject).to include_json(
json atom on path "id" is not equal to expected value:
expected: 37
got: 25
json atom on path "name" is not equal to expected value:
expected: "Smith"
got: "John"
# ./spec/simple_with_fail_spec.rb:11:in `block (2 levels) in <top (required)>'
Finished in 0.00065 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/simple_with_fail_spec.rb:10 # A json response has basic info about user
Works for both RSpec 2 and 3.
If somebody interested it is here https://github.com/waterlink/rspec-json_expectations
Here is what I have. And that kind of work.
it "should filter by name" do
users = users.search(:name => "s")
users.each {|u|
u.name.should be_starts_with("s")
}
end
However, the error message returned by rspec is really poor...
expected starts_with?("s") to return true, got false
Is there a way to get a more precise message, showing the element that failed, or at least its index?
In a binary test like this, I would create two users, one that starts with an s, the other without. I would then check that only the expected element was returned.
like
set up a user(:name => "Sam") and user(:name => "Fred")
filtered_users.map(&:name).should =~ ["Sam"]
In the case of failure, you will see something like
expected ["Sam"]
got ["Fred", "Sam"]
This is much more explicit about what you are doing
The reason you are only getting expected true but got false is because the starts_with methods returns true or false and not the actual value.
I'm not sure that this is the best way, but you can output it yourself.
users.each {|u|
p u.name if !u.name.starts_with?("s")
u.name.should be_starts_with("s")
}
Here is the way I used few times in cases like this:
describe 'user' do
before :each do
#users = users.search(:name => "s")
end
#users.each do |u|
it "should filter user with name '#{u.name}'" do
u.name.should be_starts_with("s")
end
end
end
You will have failed user name in you example description.
I found here an interesting extension to the matchers from Rspec concerning each :
http://xtargets.com/2011/08/12/rspec-meta-expectations-over-collections
So I sticked that helper into my spec_helper
RSpec::Matchers.define :each do |meta|
match do |actual|
actual.each_with_index do |i, j|
#elem = j
i.should meta
end
end
failure_message_for_should do |actual|
"at[#{#elem}] #{meta.failure_message_for_should}"
end
that allows me to write
users.should each satisfy {|u| u.name.should be_starts_with 's'}
and the error message is then :
at[1] expected #User to satisfy block
which give me the first index of failure.
With some addition to the error message, I'm sure I could output the details of that object that didn't match, and that seem a pretty good solution.
Any thoughts? I'm not a rubyist, just getting started with rails. Would be nice to get more input from
This should provide you with far better failure messages
it "should filter by name" do
users = users.search(:name => "s")
users.each do |u|
u.name.should match /^s/
end
end
I agree with Corey that calling "be_starts_with" is rough. RSpec expectations are intended to be read fluidly as a sentence. They don't all have to use "be".