Minitest spec custom matcher - ruby

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.

Related

What is 'valid?' in RSpec? Where can I look at it?

I've attempted to create a model, which needs to pass a series of validation tests in RSpec. However, I constantly get the error
expected #<Surveyor::Answer:0x0055db58e29260 #question=#<Double Surveyor::Question>, #value=5> to respond to `valid?`
My understanding (from here) was that 'valid?' checks that no errors were added to the model. I can't find any errors, however the message above persists.
This is my model
module Surveyor
class Answer
attr_accessor :question, :value
def initialize(params)
#question = params.fetch(:question)
#value = params.fetch(:value)
end
end
end
And the class Question
module Surveyor
class Question
attr_accessor :title, :type
def initialize(params)
#title = params.fetch(:title, nil)
#type = params.fetch(:type)
end
end
end
And this is the test I am attempting to pass
RSpec.describe Surveyor::Answer, '03: Answer validations' do
let(:question) { double(Surveyor::Question, type: 'rating') }
context "question validation" do
context "when the answer has a question" do
subject { described_class.new(question: question, value: 5) }
it { should be_valid }
end
end
Is my understanding of 'valid?' correct? Am I able to look at 'valid?' and perhaps see where I'm going wrong?
RSpec doesn't actually have a matcher called be_valid, instead it has some dynamic predicate matchers:
For any predicate method, RSpec gives you a corresponding matcher. Simply prefix the
method with be_ and remove the question mark. Examples:
expect(7).not_to be_zero # calls 7.zero?
expect([]).to be_empty # calls [].empty?
expect(x).to be_multiple_of(3) # calls x.multiple_of?(3)
so by calling it { should be_valid }, your subject has to respond to a valid? method. If you're testing an ActiveRecord model, those have a valid? method, but your model does not. So, if you want to test that your Answer is valid, you need to decide "what is a valid answer?" and write a method that checks for those conditions. If you want an API similar to Rails model, you might be interested in using ActiveModel::Validations

How to write regression tests for custom Chef resources?

Given the minimal example
# resources/novowel.rb
resource_name :novowel
property :name, String, name_property: true, regex: /\A[^aeiou]\z/
I would like to write the unit tests in spec/unit/resources/novowel_spec.rb
Resource 'novowel' for name should accept 'k'
Resource 'novowel' for name should accept '&'
Resource 'novowel' for name should NOT accept 'a'
Resource 'novowel' for name should NOT accept 'mm'
to ensure that the name property still works correctly even if the regex is changed for some reason.
I browsed several top notch Chef cookbooks but could not find references for such testing.
How can it be done? Feel free to provide more complex examples with explicitly subclassing Chef::Resource if that helps achieve the task.
Update 1: Could it be that Chef does NOT FAIL when a property does not fit the regex? Clearly this should not work:
link '/none' do
owner 'r<oo=t'
to '/usr'
end
but chef-apply (12.13.37) does not complain about r<oo=t not matching owner_valid_regex. It simply converges as if owner would not have been provided.
You would use ChefSpec and RSpec. I've got examples in all of my cookbooks (ex. https://github.com/poise/poise-python/tree/master/test/spec/resources) but I also use a bunch of custom helpers on top of plain ChefSpec so it might not be super helpful. Doing in-line recipe code blocks in the specs makes it waaaay easier. I've started extracting my helpers out for external use in https://github.com/poise/poise-spec but it's not finished. The current helpers are in my Halite gem, see the readme there for more info.
We wrap the DSL inside a little Ruby in order to know the name of the resource's Ruby class:
# libraries/no_vowel_resource.rb
require 'chef/resource'
class Chef
class Resource
class NoVowel < Chef::Resource
resource_name :novowel
property :letter, String, name_property: true, regex: /\A[^aeiou]\z/
property :author, String, regex: /\A[^aeiou]+\z/
end
end
end
and now we can use RSpec with
# spec/unit/libraries/no_vowel_resource_spec.rb
require 'spec_helper'
require_relative '../../../libraries/no_vowel_resource.rb'
describe Chef::Resource::NoVowel do
before(:each) do
#resource = described_class.new('k')
end
describe "property 'letter'" do
it "should accept the letter 'k'" do
#resource.letter = 'k'
expect(#resource.letter).to eq('k')
end
it "should accept the character '&'" do
#resource.letter = '&'
expect(#resource.letter).to eq('&')
end
it "should NOT accept the vowel 'a'" do
expect { #resource.letter = 'a' }.to raise_error(Chef::Exceptions::ValidationFailed)
end
it "should NOT accept the word 'mm'" do
expect { #resource.letter = 'mm' }.to raise_error(Chef::Exceptions::ValidationFailed)
end
end
describe "property 'author'" do
it "should accept a String without vowels" do
#resource.author = 'cdrngr'
expect(#resource.author).to eq('cdrngr')
end
it "should NOT accept a String with vowels" do
expect { #resource.author = 'coderanger' }.to raise_error(Chef::Exceptions::ValidationFailed)
end
end
end

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

Rspec: Difference between allow and allow_any_instance_of

I have a simple MySQL wrapper class which will run a query and return results.
class Rsql
def initialize(db)
#client = Mysql2::Client
#db = db
end
def execute_query()
client = #client.new(#db)
client.query("select 1")
end
end
I want to test some stuff involving the results of the query, but I don't want to actually connect to a database to get the results. I tried this test, but it doesn't work:
RSpec.describe Rsql do
it "does it" do
mock_database = double
rsql = Rsql.new(mock_database)
mock_mysql_client = double
allow(mock_mysql_client).to receive(:query).and_return({"1" => 1})
allow_any_instance_of(Mysql2::Client).to receive(:new).and_return(mock_mysql_client)
expect(rsql.execute_query).to eq({"1" => 1})
end
end
Replacing allow_any_instance_of() with allow() works. I was under the impression that allow_any_instance_of() was some kind of a global "pretend this class behaves in this way across the entire program" whereas allow() is for specific instances of a class.
Can someone explain this behavior to me? I'm new to Rspec, so I apologize if this answer is blatantly obvious. I tried searching for the answer, but I couldn't come up with the right search string to find one. Maybe I don't know enough to know when I've found it.
As of RSpec 3.3 , any_instance is deprecated and not recommended to use in your tests.
From the docs:
any_instance is the old way to stub or mock any instance of a class
but carries the baggage of a global monkey patch on all classes. Note
that we generally recommend against using this feature.
You should only need to use allow(some_obj) going forward and the documentation has some great examples (see here).
Such as:
RSpec.describe "receive_messages" do
it "configures return values for the provided messages" do
dbl = double("Some Collaborator")
allow(dbl).to receive_messages(:foo => 2, :bar => 3)
expect(dbl.foo).to eq(2)
expect(dbl.bar).to eq(3)
end
end
Edit, if you really want to use any_instance, do so like this:
(Mysql2::Client).allow_any_instance.to receive(:something)
Edit2, your exact stub doesn't work because you're not stubbing an instance, you're stubbing before the object is initialized. In that case you would do allow(Mysql2::Client).to receive(:new).
this Rsql class seems a service
class Rsql
def initialize(db)
#client = Mysql2::Client
#db = db
end
def execute_query()
client = #client.new(#db)
client.query("select 1")
end
end
lets create a test for it, now we should to test this function execute_query with subject ()
and to create clients in db we can use let! like this
let!(:client1) do
FactoryBot.create(...
with this we should not use double or something
require 'rails_helper'
RSpec.describe RsqlTest do
subject(:clients) do
Rsql.execute_query()
end
context 'select' do
let!(:client1) do
FactoryBot.create(...
end
it 'should return records' do
expect(clients).to include(client1)
end
end
end

Embed RSpec test in a Ruby class

I often build little single-purpose Ruby scripts like this:
#!/usr/bin/env ruby
class Widget
def end_data
DATA.read
end
def render_data source_data
source_data.upcase
end
end
w = Widget.new
puts w.render_data(w.end_data)
__END__
data set to work on.
I'd like to include RSpec tests directly inside the file while I'm working on it. Something like this (which doesn't work but illustrates what I'm trying to do):
#!/usr/bin/env ruby
class Widget
def end_data
DATA.read
end
def render_data source_data
source_data.upcase
end
def self_test
# This doesn't work but shows what I'm trying to
# accomplish. The goal is to have RSpec run these type
# of test when self_test is called.
describe "Widget" do
it "should render data properly" do
#w = Widget.new
expect(#w.render_data('test string')).to eq 'TEST STRING'
end
end
end
end
w = Widget.new
w.self_test
__END__
data set to work on.
I understand this is not the normal way to work with RSpec and isn't appropriate in most cases. That said, there are times when it would be nice. So, I'd like to know, is it possible?
There are two things. First off rspec by default won't pollute the global namespace with methods like describe and so on. The second thing is that you need to tell rspec to run the specs after they've been declared.
If you change your self_test method to be
RSpec.describe "Widget" do
it "should render data properly" do
#w = Widget.new
expect(#w.render_data('test string')).to eq 'TEST STRING'
end
end
RSpec::Core::Runner.invoke
(having of course done require 'rspec' then that will run your specs).
The invoke methods exits the process after running the specs. If you don't want to do that, or need more control over where output goes etc. you might want to drop down to the run method which allows you to control these things.

Resources