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