I have a custom RSpec matcher that checks that a job is scheduled. It's used like so:
expect { subject }.to schedule_job(TestJob)
schedule_job.rb:
class ScheduleJob
include RSpec::Mocks::ExampleMethods
def initialize(job_class)
#job_class = job_class
end
...
def matches?(proc)
job = double
expect(job_class).to receive(:new).and_return job
expect(Delayed::Job).to receive(:enqueue).with(job)
proc.call
true
end
This works fine for positive matching. But it does not work for negative matching. e.g:
expect { subject }.not_to schedule_job(TestJob) #does not work
For the above to work, the matches? method needs to return false when the expectations are not met. The problem is that even if it returns false, the expectations have been created regardless and so the test fails incorrectly.
Any ideas on how to make something like this work?
I had to look for it, but I think it's nicely described here in the rspec documentation
Format (from the docs) for separate logic when using expect.not_to:
RSpec::Matchers.define :contain do |*expected|
match do |actual|
expected.all? { |e| actual.include?(e) }
end
match_when_negated do |actual|
expected.none? { |e| actual.include?(e) }
end
end
RSpec.describe [1, 2, 3] do
it { is_expected.to contain(1, 2) }
it { is_expected.not_to contain(4, 5, 6) }
# deliberate failures
it { is_expected.to contain(1, 4) }
it { is_expected.not_to contain(1, 4) }
end
Related
While writing tests, I stopped at trying to test Service in another Service. In such a situation, I should probably just check if Service has been called because it has already been tested elsewhere. I did a little research on the Internet and found something like have_received but I have no idea how to use it in my example.
check_service.rb
Class CheckService
def initialize(params)
#params = params
end
def self.call(params)
new(params).call
end
def call
CheckUser.call(params[:user_id])
end
end
check_service_spec.rb
...
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
let(:check_user) { instance_double(CheckUser) }
before do
allow(check_user).to receive(:call).and_return(true)
end
it do
result
expect(check_user).to have_received(:call)
end
end
...
I was trying something like this (it's simple example), but I get error:
(InstanceDouble(CheckUser) (anonymous)).call(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Is there any option to test situation I presented?
Short anwser
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
## let(:check_user) { instance_double(CheckUser) } delete this
before do
allow(CheckUser).to receive(:call).and_return(true)
end
it do
result
expect(CheckUser).to have_received(:call)
end
end
Alternative
I think a better way to test this is to use DI (Dependency Injection), so you pass CheckUser as a dependency to CheckService. I prefer to write the whole test inside the it block too!
class CheckService
def initialize(params, check_handler:)
#params = params
#check_handler = check_handler
end
def self.call(params, check_handler: CheckUser)
new(params, check_handler: check_handler).call
end
def call
#check_handler.call(#params[:user_id])
end
end
describe 'call' do
it 'check user with params' do
check_user = class_double(CheckUser)
allow(check_user).to receive(:call).and_return(true)
params = { user_id: "100" }
CheckService.call(params, check_handler: check_user)
expect(check_user).to have_received(:call)
end
end
A blog post to read more about -> https://blog.testdouble.com/posts/2018-05-17-do-we-need-dependency-injection-in-ruby/
require 'requiredclass'
class Test
def get_client()
return some_client
end
def intermediate_method()
res = nil
self.class
.get_client
.retry(tries:5, on: [RequiredClass::ClientTimeout]) do |myclient|
call_count += 1
res = myclient.dosomething()
end
return res
end
def method_to_test()
x = intermediate_method()
y = false
return x && y
end
end
How can I write rspec for method_to_test here. How can I mock get_client.retry as well as calls to get_client while also mocking res variable assignment so that gets assigned the value i would like it to assign.
As written, this code is difficult to test. That's a smell and a sign that the code should be restructured. Really any time you feel tempted to mock a method in the current class, that's a sign that the thing you want to mock does not belong in that class. It should be injected (passed in) instead. Like this:
require 'requiredclass'
class Test
attr_reader :client
def initialize(client)
#client = client
end
def method_to_test
x = intermediate_method
y = false
x && y
end
def intermediate_method
res = nil
client.retry(tries: 5, on: [RequiredClass:ClientTimeout]) do |my_client|
call_count += 1
res = my_client.do_something
end
res
end
end
Given this refactored code, the tests might look like this:
RSpec.describe Test do
subject(:test) { Test.new(client) }
let(:client) { instance_double(Client, retry: true, do_something: true) }
describe '#method_to_test'
subject(:method_to_test) { test.method_to_test }
it 'returns false' do
expect(method_to_test).to be_false
end
end
end
In this code I've passed a double with a stubbed retry method into the Test class on instantiation. You could optionally use a mock, instead. That would look like this:
RSpec.describe Test do
subject(:test) { Test.new(client) }
let(:client) { instance_double(Client) }
before do
allow(client).to receive(:retry)
allow(client).to receive(:do_something)
end
describe '#method_to_test'
subject(:method_to_test) { test.method_to_test }
it 'returns false' do
expect(method_to_test).to be_false
end
end
end
There's a good write up of mocks and doubles in the RSpec documentation.
class ExternalObject
attr_accessor :external_object_attribute
def update_external_attribute(options = {})
self.external_object_attribute = [1,nil].sample
end
end
class A
attr_reader :my_attr, :external_obj
def initialize(external_obj)
#external_obj = external_obj
end
def main_method(options = {})
case options[:key]
when :my_key
self.my_private_method(:my_key) do
external_obj.update_external_attribute(reevaluate: true)
end
else
nil
end
end
private
def my_private_method(key)
old_value = key
external_object.external_object_attribute = nil
yield
external_object.external_object_attribute = old_value if external_object.external_object_attribute.nil?
end
end
I want to test following for main_method when options[:key] == :my_key:
my_private_method is called once with argument :my_key and it has a block {external_obj.update_external_attribute(reevaluate: true) } , which calls update_external_attribute on external_obj with argument reevaluate: true once.
I'm able to test my_private_method call with :my_key argument once.
expect(subject).to receive(:my_private_method).with(:my_key).once
But how do I test the remaining part of the expectation?
Thank you
It could be easier to answer your question if you post your test as well.
The setup, the execution and asseriotns/expectations.
You can find a short answer in this older question.
You can find useful to read about yield matchers.
I would suggest to mock the ExternalObject if you already haven't. But I can't tell unless you post your actual test code.
I'm going to answer your question. But, then I'm going to explain why you should not do it that way, and show you a better way.
In your test setup, you need to allow the double to yield so that the code will fall through to your block.
RSpec.describe A do
subject(:a) { described_class.new(external_obj) }
let(:external_obj) { instance_double(ExternalObject) }
describe '#main_method' do
subject(:main_method) { a.main_method(options) }
let(:options) { { key: :my_key } }
before do
allow(a).to receive(:my_private_method).and_yield
allow(external_obj).to receive(:update_external_attribute)
main_method
end
it 'does something useful' do
expect(a)
.to have_received(:my_private_method)
.with(:my_key)
.once
expect(external_obj)
.to have_received(:update_external_attribute)
.with(reevaluate: true)
.once
end
end
end
That works. The test passes. RSpec is a powerful tool. And, it will let you get away with that. But, that doesn't mean you should. Testing a private method is ALWAYS a bad idea.
Tests should only test the public interface of a class. Otherwise, you'll lock yourself into the current implementation causing the test to fail when you refactor the internal workings of the class - even if you have not changed the externally visible behavior of the object.
Here's a better approach:
RSpec.describe A do
subject(:a) { described_class.new(external_obj) }
let(:external_obj) { instance_double(ExternalObject) }
describe '#main_method' do
subject(:main_method) { a.main_method(options) }
let(:options) { { key: :my_key } }
before do
allow(external_obj).to receive(:update_external_attribute)
allow(external_obj).to receive(:external_object_attribute=)
allow(external_obj).to receive(:external_object_attribute)
main_method
end
it 'updates external attribute' do
expect(external_obj)
.to have_received(:update_external_attribute)
.with(reevaluate: true)
.once
end
end
end
Note that the expectation about the private method is gone. Now, the test is only relying on the public interface of class A and class ExternalObject.
Hope that helps.
How can I test that a method that takes an argument uses a default value if an argument is not provided?
Example
# this method shouldn't error out
# if `Post.page_results` without a parameter
class Post
def self.page_results(page=1)
page_size = 10
start = (page - 1) * page_size
finish = start + page_size
return Page.all[start..finish]
end
end
How do I check in rspec that page equals 1 if page_results is called without argument?
Testing that the page param has the default value set, is most likely not what you should test. In most cases, it is better to test the behaviour instead of the implementation (Talk from Sandy Metz about testing). In your case, you should test if the expected set of Pages is returned, when page_results is called without params (default case).
Here is an example of how you could do this:
describe Post do
describe ".page_results" do
context "when Pages exist" do
subject(:pages) { described_class.page_results(page) }
let(:expected_pages_default) { expected_pages_page_1 }
let(:expected_pages_page_1) { Page.all[0..10] }
let(:expected_pages_page_2) { Page.all[10..20] }
before do
# Create Pages
end
context "when no page param is give " do
# HINT: You need to redefine subject in this case. Setting page to nil would be wrong
subject(:pages) { described_class.page_results }
it { expect(pages).to eq expected_pages_default }
end
context "when the page param is 1" do
let(:page) { 1 }
it { expect(pages).to eq expected_pages_page_1 }
end
context "when the page param is 2" do
let(:page) { 2 }
it { expect(pages).to eq expected_pages_page_2 }
end
end
context "when no Pages exist" do
# ...
end
end
end
describe Post do
describe '#page_results' do
let(:post) { create :post }
context 'without arguments' do
it { post.page_results.should eq 1 }
end
end
end
The create :post statement is how you would do it with FactoryGirl. But you could of course mock or stub out your model.
Update
describe Post do
describe '#page_results' do
context 'without arguments' do
it { Post.page_results.should eq 1 }
end
context 'with arguments' do
it { Post.page_results('foo').should eq 'bar' }
end
end
end
I use before_validation to set defaults e.g
before_validation do
self.some_attribute ||=a_default_value
end
That leaves open the possibility to override the default
SomeClassWithDefaultAttributes.create(some_attribute:a_non_default_value)
Keep in mind that if #before_validation returns false, then the validation will fail
Suppose the model method foo() returns an array [true, false, 'unable to create widget']
Is there a way to write an rspec example that passes that array as a block that verifies [0] = true, [1] = false, and [2] matches a regex like /
Currently, I do it like:
result = p.foo
result[2].should match(/unable/i)
result[0].should == true
result[1].should == false
I can't quite get my head around how that might be doable with a block?
It would be slightly over engineered but try to run this spec with --format documentation. You will see a very nice specdocs for this method ;)
describe '#some_method' do
describe 'result' do
let(:result) { subject.some_method }
subject { result }
it { should be_an_instance_of(Array) }
describe 'first returned value' do
subject { result.first }
it { should be_false }
end
describe 'second returned value' do
subject { result.second }
it { should be_true }
end
describe 'third returned value' do
subject { result.third }
it { should == 'some value' }
end
end
end
Do you mean that your result is an array and you have to iterate over to test it's various cases?
Then, you can do that by following, right:
result = p.foo
result.each_with_index do |value, index|
case index
when 0 then value.should == true
when 1 then value.should == false
when 2 then value.shoud match(/unable/i)
end
end