Verbose parent/child matcher errors for Rspec - ruby

Given the following code, when I get a failure, I would like to see both the parent and child diff's shown. Currently, if I have a mismatch at the children level, I only get feedback that my parent did not contain the expected children, not what my actual children were.:
RSpec::Matchers.define :have_categories do |expected|
match do |actual|
expected.each do |ex|
lines = ex.map do |ex_line|
an_instance_of(Model::SubCategory).and(have_attributes(ex_line))
end
expect(actual.categories).to include(an_instance_of(Model::Category).and(
have_attributes(sub_categories: contain_exactly(*lines))))
end
end
end

Basically, you need to implement failure_message method in order to format expected error message, and here you can create a string of diff you would like to see:
RSpec::Matchers.define :have_categories do |expected|
match do |actual|
expected.each do |ex|
lines = ex.map do |ex_line|
an_instance_of(Model::SubCategory).and(have_attributes(ex_line))
end
expect(actual.categories).to include(an_instance_of(Model::Category).and(
have_attributes(sub_categories: contain_exactly(*lines))))
end
failure_message do |actual|
# here you are able to use both `actual` and `expected`
# to create your error message
end
end
end
Please , provide more information about your domain, so I can try to help you with preparing proper error message.
Hope that helps!

Related

Re-use failure message in rspec custom matcher

I have a custom matcher that uses expects in it's match block (the code here is simplified)
RSpec::Matchers.define :have_foo_content do |expected|
match do |actual|
expect(actual).to contain_exactly(expected)
expect(actual.foo).to contain_exactly(expected.foo)
end
end
Normally the error message would look like this
expected collection contained: ["VLPpzkjahD"]
actual collection contained: ["yBzPmoRnSK"]
the missing elements were: ["VLPpzkjahD"]
the extra elements were: ["yBzPmoRnSK"]
But when using a custom matcher, it only prints this, and important debug information gets lost:
expected MyObject to have_foo_content "foobar"
So, is it possible to re-use a error message from the match block as a failure message? I know I can provide custom failure messages with
failure_message do |actual|
# ...
end
But I don't know how you could access the failure message the error above has raised.
You can rescue RSpec::Expectations::ExpectationNotMetError in your match to catch the failed expectation message:
match do |object|
begin
expect(object).to be_nil
rescue RSpec::Expectations::ExpectationNotMetError => e
#error = e
raise
end
end
failure_message do
<<~MESSAGE
Expected object to meet my custom matcher expectation but failed with error:
#{#error}
MESSAGE
end
Don't forget to re-raise in the rescue, otherwise it won't work
There is no direct method available to yield original error, I would suggest you to write your own logic to generate similar message.
If you still want to use the existing method, there is a private method which you can call and it will return the default error message. You may need to set some instance variables expected_value, actual_value etc.
RSpec::Matchers::BuiltIn::ContainExactly.new(expected_value).failure_message
reference code

use rspec built in matcher within custom matcher

I have a test suite with too many assertions like this
expect(array_1).to match_array array_2
expect(array_3).to match_array array_4
expect(array_5).to match_array array_5
and so forth.
I'd like to wrap these checks in a custom matcher but within that custom matcher would like to use the match_array matcher as I really like the error message it returns listing missing and extra elements in case of a mismatch.
Something like this:
RSpec::Matchers.define :have_data do |data|
match do |actual|
actual.eql? data # I don't want to do this.
actual.match_array? data # <<- I'd like do do something like this to retain the matcher behaviour
end
end
Any way I can do this? match_array? doesn't exist, of course.
Looking at the implementation of the match_array matcher:
# An alternate form of `contain_exactly` that accepts
# the expected contents as a single array arg rather
# that splatted out as individual items.
#
# #example
# expect(results).to contain_exactly(1, 2)
# # is identical to:
# expect(results).to match_array([1, 2])
#
# #see #contain_exactly
def match_array(items)
contain_exactly(*items)
end
And the contain_exactly method makes a call to the Rspec::BuiltIn::ContainExactly module:
def contain_exactly(*items)
BuiltIn::ContainExactly.new(items)
end
You could iterate over the data you need to match using calls to this module, and still use the error messages from the match_array method.
Alternatively, you could implement your own module based on the BuiltIn::ContainExactly module.
I wanted to use the contain_exactly functionality with a remapping of the actual array to ids for the array items. This is similar to what you are trying to do, so perhaps it will help. I put this in /support/matchers/match_array_ids.rb.
module MyMatchers
class MatchArrayIds < RSpec::Matchers::BuiltIn::ContainExactly
def match_when_sorted?
values_match?(safe_sort(expected), safe_sort(actual.map(&:id)))
end
end
def match_array_ids(items)
MatchArrayIds.new(items)
end
end
I also created a test to test the matcher was functioning as expected. I put this in spec/matcher_tests/match_array_ids_spec.rb.
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MyMatchers::MatchArrayIds do
before do
class ObjectWithId
attr_reader :id
def initialize(id)
#id = id
end
end
end
context 'when array of objects with ids and array of ids match' do
let(:objects_with_ids) { [ObjectWithId.new('id1'), ObjectWithId.new('id2')] }
let(:ids) { ['id1', 'id2'] }
it 'returns true' do
expect(objects_with_ids).to match_array_ids(ids)
end
end
context 'when array of objects with ids exist and array of ids is empty' do
let(:objects_with_ids) { [ObjectWithId.new('id1'), ObjectWithId.new('id2')] }
let(:ids) { [] }
it 'returns false' do
expect(objects_with_ids).not_to match_array_ids(ids)
end
end
context 'when array of objects with ids is empty and array of ids exist' do
let(:objects_with_ids) { [] }
let(:ids) { ['id1', 'id2'] }
it 'returns false' do
expect(objects_with_ids).not_to match_array_ids(ids)
end
end
context 'when array of objects with ids and array of ids DO NOT match' do
let(:objects_with_ids) { [ObjectWithId.new('id1'), ObjectWithId.new('id2')] }
let(:ids) { ['id1', 'id3'] }
it 'returns false' do
expect(objects_with_ids).not_to match_array_ids(ids)
end
end
end
You can find the examples for making a custom from scratch matcher that I based this on at...
https://github.com/rspec/rspec-expectations/blob/45e5070c797fb4cb6166c8daca2ea68e31aeca40/lib/rspec/matchers.rb#L145-L196

Can I use a built-in RSpec matcher in a custom matcher?

I have the following expectation in a feature spec (pretty low-level but still necessary):
expect(Addressable::URI.parse(current_url).query_values).to include(
'some => 'value',
'some_other' => String
)
Note the second query value is a fuzzy match because I just want to make sure it's there but I can't be more specific about it.
I'd like to extract this into a custom matcher. I started with:
RSpec::Matchers.define :have_query_params do |expected_params|
match do |url|
Addressable::URI.parse(url).query_values == expected_params
end
end
but this means I cannot pass {'some_other' => String} in there. To keep using a fuzzy match, I'd have to use the include matcher in my custom matcher.
However, anything within RSpec::Matchers::BuiltIn is marked as private API, and Include specifically is documented as:
# Provides the implementation for `include`.
# Not intended to be instantiated directly.
So, my question is: Is using a built-in matcher within a custom matcher supported in RSpec? How would I do that?
RSpec::Matchers appears to be a supported API (its rdoc doesn't say otherwise), so you can write your matcher in Ruby instead of in the matcher DSL (which is supported; see the second paragraph of the custom matcher documentation) and use RSpec::Matchers#include like this:
spec/support/matchers.rb
module My
module Matchers
def have_query_params(expected)
HasQueryParams.new expected
end
class HasQueryParams
include RSpec::Matchers
def initialize(expected)
#expected = expected
end
def matches?(url)
actual = Addressable::URI.parse(url).query_values
#matcher = include #expected
#matcher.matches? actual
end
def failure_message
#matcher.failure_message
end
end
end
end
spec/support/matcher_spec.rb
include My::Matchers
describe My::Matchers::HasQueryParams do
it "matches" do
expect("http://example.com?a=1&b=2").to have_query_params('a' => '1', 'b' => '2')
end
end
Yes, you can call built-in rspec matchers from within a custom matcher. Put another way, you can use the normal Rspec DSL instead of pure Ruby when writing your matcher. Check out this gist (not my gist, but it helped me!).
I've got a really complex controller with a tabbed interface where the defined and selected tab depend on the state of the model instance. I needed to test tab setup in every state of the :new, :create, :edit and :update actions. So I wrote these matchers:
require "rspec/expectations"
RSpec::Matchers.define :define_the_review_tabs do
match do
expect(assigns(:roles )).to be_a_kind_of(Array)
expect(assigns(:creators )).to be_a_kind_of(ActiveRecord::Relation)
expect(assigns(:works )).to be_a_kind_of(Array)
expect(assigns(:available_tabs)).to include("post-new-work")
expect(assigns(:available_tabs)).to include("post-choose-work")
end
match_when_negated do
expect(assigns(:roles )).to_not be_a_kind_of(Array)
expect(assigns(:creators )).to_not be_a_kind_of(ActiveRecord::Relation)
expect(assigns(:works )).to_not be_a_kind_of(Array)
expect(assigns(:available_tabs)).to_not include("post-new-work")
expect(assigns(:available_tabs)).to_not include("post-choose-work")
end
failure_message do
"expected to set up the review tabs, but did not"
end
failure_message_when_negated do
"expected not to set up review tabs, but they did"
end
end
RSpec::Matchers.define :define_the_standalone_tab do
match do
expect(assigns(:available_tabs)).to include("post-standalone")
end
match_when_negated do
expect(assigns(:available_tabs)).to_not include("post-standalone")
end
failure_message do
"expected to set up the standalone tab, but did not"
end
failure_message_when_negated do
"expected not to set up standalone tab, but they did"
end
end
RSpec::Matchers.define :define_only_the_review_tabs do
match do
expect(assigns).to define_the_review_tabs
expect(assigns).to_not define_the_standalone_tab
expect(assigns(:selected_tab)).to eq(#selected) if #selected
end
chain :and_select do |selected|
#selected = selected
end
failure_message do
if #selected
"expected to set up only the review tabs and select #{#selected}, but did not"
else
"expected to set up only the review tabs, but did not"
end
end
end
RSpec::Matchers.define :define_only_the_standalone_tab do
match do
expect(assigns).to define_the_standalone_tab
expect(assigns).to_not define_the_review_tabs
expect(assigns(:selected_tab)).to eq("post-standalone")
end
failure_message do
"expected to set up only the standalone tab, but did not"
end
end
RSpec::Matchers.define :define_all_tabs do
match do
expect(assigns).to define_the_review_tabs
expect(assigns).to define_the_standalone_tab
expect(assigns(:selected_tab)).to eq(#selected) if #selected
end
chain :and_select do |selected|
#selected = selected
end
failure_message do
if #selected
"expected to set up all three tabs and select #{#selected}, but did not"
else
"expected to set up all three tabs, but did not"
end
end
end
And am using them like so:
should define_all_tabs.and_select("post-choose-work")
should define_all_tabs.and_select("post-standalone")
should define_only_the_standalone_tab
should define_only_the_review_tabs.and_select("post-choose-work")
should define_only_the_review_tabs.and_select("post-new-work")
Super-awesome to be able to just take several chunks of repeated expectations and roll them up into a set of custom matchers without having to write the matchers in pure Ruby.
This saves me dozens of lines of code, makes my tests more expressive, and allows me to change things in one place if the logic for populating these tabs changes.
Also note that you have access in your custom matcher to methods/variables such as assigns and controller so you don't need to pass them in explicitly.
Finally, I could have defined these matchers inline in the spec, but I chose to put them in spec/support/matchers/controllers/posts_controller_matchers.rb
You can use the matcher DSL instead of writing your own Matcher class. It is a bit simpler.
RSpec::Matchers.define :have_query_params do |expected|
match do |actual|
# your code
RSpec::Matchers::BuiltIn::Include.new(expected).matches?(actual)
end
end

minitest assert custom assertion fails

I'm using custom assertions in my minitests and I want to unit test my assertions. Of course I can test the happy path but I want to assert that a test actually fails.
module Minitest
module Assertions
def assert_exists(value, msg = nil)
assert(!value.to_s.empty?, msg)
end
end
end
In my test I want to write something like
describe 'Assertions' do
it 'is empty' do
assert_raises assert_exists('')
end
end
Is there a way to do this?
Something like this? (You need to specify the exception you are expecting, and pass the call as a block):
describe 'Assertions' do
it 'is empty' do
assert_raises(Minitest::Assertion) do
assert_exists('')
end
end
end
This will include the call to assert in your assert_raises in the summary, which may not be exactly what you expect, but otherwise works.

Minitest spec custom matcher

I have a line in my test:
page.has_reply?("my reply").must_equal true
and to make it more readable I want to use a custom matcher:
page.must_have_reply "my reply"
Based on the docs for https://github.com/zenspider/minitest-matchers I expect I need to write a matcher which looks something like this:
def have_reply(text)
subject.has_css?('.comment_body', :text => text)
end
MiniTest::Unit::TestCase.register_matcher :have_reply, :have_reply
The problem is that I can't see how to get a reference to the subject (i.e. the page object). The docs say "Note subject must be the first argument in assertion" but that doesn't really help.
There is a little example, you can create a class which should responds to set of methods matches?, failure_message_for_should, failure_message_for_should_not.
In matches? method you can get the reference to the subject.
class MyMatcher
def initialize(text)
#text = text
end
def matches? subject
subject =~ /^#{#text}.*/
end
def failure_message_for_should
"expected to start with #{#text}"
end
def failure_message_for_should_not
"expected not to start with #{#text}"
end
end
def start_with(text)
MyMatcher.new(text)
end
MiniTest::Unit::TestCase.register_matcher :start_with, :start_with
describe 'something' do
it 'must start with...' do
page = 'my reply'
page.must_start_with 'my reply'
page.must_start_with 'my '
end
end
There are many ways to get what you want here. The easiest way is to not mess with assertions, expectations, or matchers at all and just use an assert. So, assuming you already have the has_reply? method defined, you could just use this:
assert page.has_reply?("my reply")
But, that doesn't get you the must_have_reply syntax you are asking for. And I doubt you really have a has_reply? method. So, let's start.
Your asked "how to get a reference to the subject (i.e. the page object)". In this case the subject is the object that the must_have_reply method is defined on. So, you should use this instead of subject. But its not as straightforward as all that. Matchers add a level of indirection that we don't have with the usual Assertions (assert_equal, refute_equal) or Expectations (must_be_equal, wont_be_equal). If you want to write a Matcher you need to implement the Matcher API.
Fortunately for you you don't really have to implement the API. Since it seems you are already intending on relying on Cabybara's have_css matcher, we can simply use Capybara's HaveSelector class and let it implement the proper API. We just need to create our own Matchers module with a method that returns a HaveSelector object.
# Require Minitest Matchers to make this all work
require "minitest/matchers"
# Require Capybara's matchers so you can use them
require "capybara/rspec/matchers"
# Create your own matchers module
module YourApp
module Matchers
def have_reply text
# Return a properly configured HaveSelector instance
Capybara::RSpecMatchers::HaveSelector.new(:css, ".comment_body", :text => text)
end
# Register module using minitest-matcher syntax
def self.included base
instance_methods.each do |name|
base.register_matcher name, name
end
end
end
end
Then, in your minitest_helper.rb file, you can include your Matchers module so you can use it. (This code will include the matcher in all tests.)
class MiniTest::Rails::ActiveSupport::TestCase
# Include your module in the test case
include YourApp::Matchers
end
Minitest Matchers does all the hard lifting. You can now you can use your matcher as an assertion:
def test_using_an_assertion
visit root_path
assert_have_reply page, "my reply"
end
Or, you can use your matcher as an expectation:
it "is an expectation" do
visit root_path
page.must_have_reply "my reply"
end
And finally you can use it with a subject:
describe "with a subject" do
before { visit root_path }
subject { page }
it { must have_reply("my reply") }
must { have_reply "my reply" }
end
Important: For this to work, you must be using 'gem minitest-matchers', '>= 1.2.0' because register_matcher is not defined in earlier versions of that gem.

Resources