How to test error handling with RSpec? - ruby

I try to test this code:
mail = Mail.new
mail.from = #from
mail.to = to
<...>
begin
mail.deliver
rescue
return false
else
return true
end
for error handling with RSpec:
it 'should return true if success' do
expect(mailer.send_confirmation_request(token)).to be true
end
it 'should return false if fails' do
allow_any_instance_of(Mail).to receive(:deliver).and_raise('Mail error')
expect(mailer.send_confirmation_request(token)).to be false
end
But it doesn't work: second test failed. Why does allow_any_instance_of (and allow(Mail)) not work in this way? How that can be tested? Should am I fix my code to be more testable? I can test it by stubbing Mail#new with rspec double, but it leads to stubbing all mail methods and not looks like a right way to do.
If it does matter, I use mikel's mail gem and make tests according to his recommendations.

Mail.new doesn't return an instance of Mail, it returns an instance of Mail::Message, so you're calling allow_any_instance_of on the wrong class.
I assume the library does this to simplify the interface, but it is a little counterintuitive.

When you mock or stub a method using expect_any_instance_ orallow_any_instance_`, you're actually replacing the normal value of that method with what you return in the stub.
You can use and_call_original if you want to preserve the original implementation of the method call.
Relevant RSpec docs:
https://www.relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/calling-the-original-method

Related

Where and how to expect on Rspec / VCR stubbed response?

I'm writing a client in ruby for an API that we're using. We use rspec and VCR with webmock to mock request/responses to the API.
What is the best or appropriate way to test the response back from the API when the response payload is really large?
Is there a best practice around where to put the large expected payload and how to test this?
require 'spec_helper'
describe Service::API, vcr: true do
describe '.method' do
it 'returns valid response' do
#returns large body payload
response = subject.method
expect(response).to eq ???
end
end
end
You shouldn't be testing the payload, you need to test that the method does what you expect with that payload. VCR is going to take care of storing the payload. You may need to assert that you send what you expect to the API, and assert what you do with the result. You should also test the fail scenarios, like the API times out, or network error etc. But the payload itself you shouldn't really need to touch in the test.
You will probably find that it helps to break the test down into scenarios;
given a call with the correct params
given a bad call e.g. missing resource at the API end
given a network error
I tend to find it easier to just write some of the obvious scenarios and then start from the assert for each one. Something like below, somewhat abridged and using RSpec but you should get the idea;
describe Transaction do
# Create a real object in the api.
let(:transaction) { create :transaction }
describe '.find', :vcr do
context 'given a valid id' do
subject { Transaction.find(transaction.id) }
it 'returns a Transaction object' do
is_expected.to eq(transaction)
end
end
context 'given an invalid transaction_id' do
subject { Transaction.find('my-bad-transaction') }
it 'should rescue Gateway::NotFoundError' do
is_expected.to be_nil
end
it 'raises no NotFoundError' do
expect { subject }.to raise_error(Gateway::NotFoundError)
end
end
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.

Rspec 3.0 How to mock a method replacing the parameter but with no return value?

I've searched a lot and just cannot figure this out although it seems basic. Here's a way simplified example of what I want to do.
Create a simple method that does something but doesn't return anything, such as:
class Test
def test_method(param)
puts param
end
test_method("hello")
end
But in my rspec test I need to pass a different parameter, such as "goodbye" instead of "hello." I know this has to do with stubs and mocks, and I've looking over the documentation but can't figure it out: https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/method-stubs
If I do:
#test = Test.new
allow(#test).to_receive(:test_method).with("goodbye")
it tells me to stub out a default value but I can't figure out how to do it correctly.
Error message:
received :test_method with unexpected arguments
expected: ("hello")
got: ("goodbye")
Please stub a default value first if message might be received with other args as well.
I am using rspec 3.0, and calling something like
#test.stub(:test_method)
is not allowed.
How to set a default value that is explained at
and_call_original can configure a default response that can be overriden for specific args
require 'calculator'
RSpec.describe "and_call_original" do
it "can be overriden for specific arguments using #with" do
allow(Calculator).to receive(:add).and_call_original
allow(Calculator).to receive(:add).with(2, 3).and_return(-5)
expect(Calculator.add(2, 2)).to eq(4)
expect(Calculator.add(2, 3)).to eq(-5)
end
end
Source where I came to know about that can be found at https://makandracards.com/makandra/30543-rspec-only-stub-a-method-when-a-particular-argument-is-passed
For your example, since you don't need to test the actual result of test_method, only that puts gets called in it passing in param, I would just test by setting up the expectation and running the method:
class Test
def test_method(param)
puts param
end
end
describe Test do
let(:test) { Test.new }
it 'says hello via expectation' do
expect(test).to receive(:puts).with('hello')
test.test_method('hello')
end
it 'says goodbye via expectation' do
expect(test).to receive(:puts).with('goodbye')
test.test_method('goodbye')
end
end
What it seems you're attempting to do is set up a test spy on the method, but then I think you're setting up the method stub one level too high (on test_method itself instead of the call to puts inside test_method). If you put the stub on the call to puts, your tests should pass:
describe Test do
let(:test) { Test.new }
it 'says hello using a test spy' do
allow(test).to receive(:puts).with('hello')
test.test_method('hello')
expect(test).to have_received(:puts).with('hello')
end
it 'says goodbye using a test spy' do
allow(test).to receive(:puts).with('goodbye')
test.test_method('goodbye')
expect(test).to have_received(:puts).with('goodbye')
end
end

user must_send with a method that calls super in minitest

Lets say I have the following module:
module SillyDemo
class Monkey
def screech(sound)
sound
end
end
class Ape < Monkey
def process(sound)
sound
end
def screech(sound)
process(sound)
super
sound
end
end
end
And then the following minitest:
require_relative 'sillydemo'
require "minitest/spec"
require "minitest/autorun"
describe "Ape" do
before do
#ape = Ape.new
#screech = "YEEEEEEE"
end
it "screeches" do
#ape.screech(#screech)
must_send [#ape, :process, #screech]
must_send [#ape, :super, #screech]
end
end
This errors out with:
NoMethodError: undefined method `super' for #<SillyDemo::Ape:0x007feeb10943c0>
(eval):4:in `must_send'
I have also tried:
must_send [#ape, :"SillyDemo::Monkey.screech", #screech]
which errors out with:
NoMethodError: undefined method `SillyDemo::Ape.run' for #<SillyDemo::Ape:0x007fc5a1874e20>
(eval):4:in `must_send'
My question is, how can I use minitest to test a call to super?
In Ruby super is a keyword, not a method. Also, the must_send expectation isn't verifying that the method was called, it just verifies that the return value from the method is truthy.
http://www.ruby-doc.org/stdlib-2.0.0/libdoc/minitest/rdoc/MiniTest/Expectations.html#method-i-must_send
http://www.ruby-doc.org/stdlib-2.0.0/libdoc/minitest/rdoc/MiniTest/Assertions.html#method-i-assert_send
Mocks are usually used to verify that a method was called. However, Minitest::Mock doesn't allow for this type of check very easily by design. Here is how you can do this though.
it "screeches" do
# 1) Create mock
sound_mock = Minitest::Mock.new
sound_mock.expect :process, true, [String]
# 2) Place mock
#ape.instance_exec(sound_mock) do |sound_mock|
#mock = sound_mock
def process sound
#mock.process sound
end
end
# 3) Verify mock was called
#ape.screech(#screech)
sound_mock.verify
end
Pretty ugly, right? This is by design. Sort of like syntactic vinegar. The reason is that this use of mocks isn't very informative. It is checking the implementation of the code. You would like to be able to refactor the code without changing behavior and have the tests continue to pass. However, this test will very likely fail when the implementation is changed. To discourage folks from making this kind of mistake it was decided by the Minitest authors to keep this type of check difficult.
Other mocking libraries such as RR or Mocha make this type of check much easier.

rails rspec - how to check for a model constant?

How can I do something like:
it { should have_constant(:FIXED_LIST) }
In my model (active record) I have FIXED_LIST = 'A String'
It's not a db attribute or a method and I haven't been able to use responds_to or has_attribute to test for it (they fail). What can I use the to check for it. - btw I have the shoulda-matchers installed.
Based on David Chelimsky's answer I've got this to work by slightly modifying his code.
In a file spec/support/utilities.rb (or some other in spec/support) you can put:
RSpec::Matchers.define :have_constant do |const|
match do |owner|
owner.const_defined?(const)
end
end
Note the use of "RSpec::Matchers.define" in stead of "matchers"
This allows to test for constants in your specs, like:
it "should have a fixed list constant" do
YourModel.should have_constant(:FIXED_LIST)
end
Note the use of "have_constant" in stead of "have_const"
It reads a little silly, but:
describe MyClass do
it { should be_const_defined(:VERSION) }
end
The reason is that Rspec has "magic" matchers for methods starting with be_ and have_. For example, it { should have_green_pants } would assert that the has_green_pants? method on the subject returns true.
In the same fashion, an example such as it { should be_happy } would assert that the happy? method on the subject returns true.
So, the example it { should be_const_defined(:VERSION) } asserts that const_defined?(:VERSION) returns true.
If you want to say have_constant you can define a custom matcher for it:
matcher :have_constant do |const|
match do |owner|
owner.const_defined?(const)
end
end
MyClass.should have_const(:CONST)
If you're trying to use the one-liner syntax, you'll need to make sure the subject is a class (not an instance) or check for it in the matcher:
matcher :have_constant do |const|
match do |owner|
(owner.is_a?(Class) ? owner : owner.class).const_defined?(const)
end
end
See http://rubydoc.info/gems/rspec-expectations/RSpec/Matchers for more info on custom matchers.
HTH,
David
Another option to simply make sure the constant is defined – not worrying about what it's defined with:
it 'has a WHATEVER constant' do
expect(SomeClass::WHATEVER).not_to be_nil
end
A warning to anyone trying to test that constants are defined: If your code references an undefined constant while defining a class, then your specs will crash before they get to your test.
This can lead you to believe that
expect { FOO }.to_not raise_error
is failing to catch the NameError, because you'll get a big stack trace, instead of a nice "expected not to raise error, but raised NameError."
Amidst the huge stack trace, it can be difficult to notice that your test is actually crashing on line 1: requre "spec/spec_helper" because your entire application is failing to load before it gets to your actual test.
This can happen if you have dynamically defined constants, such as is done by ActiveHash::Enum, and you then use them in the definition of another constant. Don't bother testing that they exist, every spec in your app will crash if one of them fails to be defined.
You could use
defined? YOUR_MODEL::FIXED_LIST
In RSpec 2, I was able to get this to work in one line as follows:
it { subject.class.should be_const_defined(:MY_CONST) }
That is, check against the class, instead of the instance.
In My model
class Role < ActiveRecord::Base
ROLE_ADMIN = "Administrador"
end
In My rspec
RSpec.describe Role, type: :model do
let(:fake_class) { Class.new }
describe "set constants" do
before { stub_const("#{described_class}", fake_class) }
it { expect(described_class::ROLE_ADMIN).to eq("Administrador") }
end
end
For ruby 2.1.5 and rspec 3.5.0 I am able to test that constant SEARCH_CHARS_TO_IGNORE is defined in the class DiffAlertsDatatable as follows:
expect(DiffAlertsDatatable.const_defined?(:SEARCH_CHARS_TO_IGNORE)).to eq(true)

Resources