I have the following:
class PaymentController < ActionController::API
include ObjectActions
def error_notification(message)
puts "An error has occurred: #{message}"
end
end
module ObjectActions
extend ActiveSupport::Concern
def process
if valid?
# process payment
else
error_notification("Payment is not valid")
end
end
end
Now, I'm trying to mock/stub the "external" error_notification method inside ObjectActions module/concern.
RSpec.describe ObjectActions, type: :concern do
include ObjectActions
before do
allow(described_class).to receive(:valid?).and_return(false)
# I KNOW THIS IS NOT RIGHT, HOW CAN I PROPERLY MOCK IT?
allow(described_class).to receive(:error_notification).and_return("Blah blah")
end
context '#process' do
it { expect { process }.to eq("Blah blah") }
end
end
Short answer would be
allow(self).to receive(:error_notification).and_return("Blah blah")
Why?
You're including the module, you want to test in the current test
RSpec.describe ObjectActions, type: :concern do
include ObjectActions
So this is what you're supposed to mock. But it's a bit better way to do it, which I described not long ago in this answer: https://stackoverflow.com/a/48914463/299774
Related
I've been trying to stub a private module method for the whole day now but with not progress.
Here is a snippet of my application controller class
class ApplicationController < ActionController::Base
include Cesid::Application
end
Cesid > Application.rb
module Cesid
module Application
extend ActiveSupport::Concern
included do
before_action :track_marketing_suite_cesid, only: [:new]
end
private
def track_marketing_suite_cesid
return unless id_token_available?
## #cesid_auth = Auth.new(#id_token)
#cesid_auth = Auth.new(id_token)
return unless #cesid_auth.present? && #cesid_auth.valid?
#cesid_admin = Admin.where(email: #cesid_auth.email).first_or_initialize
end
def id_token_available?
## #id_token.present?
id_token.present?
end
def id_token
#id_token ||= id_token_param
end
def id_token_param
cookies[:id_token]
end
end
end
Now, I'm trying to create a simple unit test for the method
id_token_available?
And I am just trying to set the id_token_param to a random value.
I've tried using this code as stated Is there a way to stub a method of an included module with Rspec?
allow_any_instance_of(Cesid).to receive(:id_token_param).and_return('hello')
but I just get this error
NoMethodError:
undefined method `allow_any_instance_of' for #<RSpec::ExampleGroups::CesidApplication::CesidAuthorizations::GetCesidApplication:0x00007fa3d200c1c0> Did you mean? allow_mass_assignment_of
Rspec file
require 'rails_helper'
describe Cesid::Application, :type => :controller do
describe 'cesid application' do
before do
allow_any_instance_of(ApplicationController).to receive(:id_token_param).and_return('hello')
end
it 'returns true if the id_token is present' do
expect(Cesid::Application.send('id_token_available?')).to eql(true)
end
end
end
Rspec version
3.5.4
This is honestly starting to drive me crazy
I see three issues:
You call allow_any_instance_of in a context in which it is not defined. allow_any_instance_of can be used in before blocks. I need to see your RSpec code to be more specific.
Actually your code is called on the ApplicationController, not on the module, therefore you need to change your stub to
allow_any_instance_of(ApplicationController).to receive(:id_token_param).and_return('hello')
Currently id_token_param will not be called at all, because id_token_available? checks the instance variable and not the return value of the id_token method that calls the id_token_param. Just change the id_token_available? to:
def id_token_available?
id_token.present?
end
There's a much better way of going about this test. The type: :controller metadata on your spec gives you an anonymous controller instance to work with.
Here's an example of how you could write this to actually test that the before_action from your module is used:
describe Cesid::Application, type: :controller do
controller(ApplicationController) do
def new
render plain: 'Hello'
end
end
describe 'cesid before_action' do
before(:each) do
routes.draw { get 'new' => 'anonymous#new' }
cookies[:id_token] = id_token
allow(Auth).to receive(:new).with(id_token)
.and_return(instance_double(Auth, valid?: false))
get :new
end
context 'when id token is available' do
let(:id_token) { 'hello' }
it 'sets #cesid_auth' do
expect(assigns(:cesid_auth)).to be_present
end
end
context 'when id token is unavailable' do
let(:id_token) { '' }
it 'does not set #cesid_auth' do
expect(assigns(:cesid_auth)).to be_nil
end
end
end
end
I have FooConcern and BarService as follows:
module FooConcern
extend ActiveSupport::Concern
def notify_slack(message)
# send message to slack
end
end
class BarService
include FooConcern
def run
# do some stuff
message = 'blah, blah'
notify_slack(message)
end
end
How can I write to mock notify_slack so I don't actually call slack API when I run Rspec test for BarService#run ?
RSpec.describe BarService do
describe 'run' do
subject { described_class.new.run }
it do
# some tests on things other than notifying slack
end
end
end
You can use allow to mock that method.
RSpec.describe BarService do
describe 'run' do
subject { described_class.new }
it do
allow(subject).to receive(:notify_slack).and_return(true)
# some tests on things other than notifying slack
subject.run
end
end
end
This module gets included in a form object within rails.
What is the right way to test it using rspec?
1) Do I test it directly on each model that includes it?
or
2) Do I test the delegation method directly? (i would prefer direct if possible)
If I test it directly, how? I tried and get the below error...
Form Object Module
module Registration
class Base
module ActAsDelegation
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def form_fields_mapping
[
{name: :first, model: :user},
{name: :address, model: :address}
]
end
def fields_of_model(model)
form_fields_mapping.select {|record| record[:model] == model }.map {|record| record[:name] }
end
def delegate_fields_to(*models)
models.each do |model|
fields_of_model(model).each do |attr|
delegate attr.to_sym, "#{attr}=".to_sym, to: model if attr.present?
end
end
end
end
end
end
end
Form Object
module Registration
class Base
include ActiveModel::Model
include ActAsDelegation
def initialize(user=nil, attributes={})
error_msg = "Can only initiate inherited Classes of Base, not Base Directly"
raise ArgumentError, error_msg if self.class == Registration::Base
#user = user
setup_custom_accessors
unless attributes.nil?
(self.class.model_fields & attributes.keys.map(&:to_sym)).each do |field|
public_send("#{field}=".to_sym, attributes[field])
end
end
validate!
end
end
end
RSPEC TESTING
require "rails_helper"
RSpec.describe Registration::Base::ActAsDelegation, type: :model do
describe "Class Methods" do
context "#delegate_fields_to" do
let(:user) {spy('user')}
let(:address) {spy('address')}
let(:delegation_fields) { [
{name: :first, model: :user},
{name: :address, model: :address}
]}
it "should delegate" do
allow(subject).to receive(:form_fields_mapping) { delegation_fields }
Registration::Base::ActAsDelegation.delegate_fields_to(:user,:address)
expect(user).to have_received(:first)
expect(address).to have_received(:address)
end
end
end
end
ERROR
Failure/Error:
Registration::Base::ActAsDelegation.delegate_fields_to(:user,:address)
NoMethodError:
undefined method `delegate_fields_to' for Registration::Base::ActAsDelegation:Module
Did you mean? delegate_missing_to
(I have other code issues in this example, but below resoved the main issue)
As your module is designed to be included, just include it in an empty class in tests. I prepared a simplified example which I verified to work:
module ToBeIncluded
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def a_class_method
:class
end
end
end
class TestSubject
include ToBeIncluded
end
require 'rspec/core'
RSpec.describe ToBeIncluded do
subject { TestSubject }
it 'returns correct symbol' do
expect(subject.a_class_method).to eq(:class)
end
end
In your case probably something along those lines should be fine:
require "rails_helper"
RSpec.describe Registration::Base::ActAsDelegation, type: :model do
class TestClass
include Registration::Base::ActAsDelegation
end
describe "Class Methods" do
context "#delegate_fields_to" do
let(:user) {spy('user')}
let(:address) {spy('address')}
let(:delegation_fields) { [
{name: :first, model: :user},
{name: :address, model: :address}
]}
it "should delegate" do
allow(TestClass).to receive(:form_fields_mapping) { delegation_fields }
TestClass.delegate_fields_to(:user,:address)
expect(user).to have_received(:first)
expect(address).to have_received(:address)
end
end
end
end
Also, you could make anonymous class if you are afraid of name clashes.
Im writing a test for this service.
def run
sort_offers(product).each do |product_code|
......
offer.update(poduct_params)
Importer::Partner.get_details(product_code).new
end
end
It's calling a service which in some cases will override the values that were saved when running offer.update(product_prams). How would I go about skipping the service call within my test?
Here is the example of my test
context 'is valid' do
.... .....
before do
Importer::ProductCodes(product).run
end
it ......
end
I would stub Importer::Partner.get_details to return a double that responds to new:
context 'is valid' do
before do
allow(Importer::Partner).to receive(:get_details).and_return(double(new: nil))
end
# it ...
end
Depending on your needs you might want to add an expectation that the mock was called with the correct parameters and that new was actually called on the mock too:
context 'is valid' do
let(:mock) { double(new: nil) }
before do
allow(Importer::Partner).to receive(:get_details).and_return(double(new: nil))
end
it "calls the service" do
an_instance.run
expect(Importer::Partner).to have_received(:get_details).with(
foo: 'bar' # the arguments you would expect
)
expect(mock).to have_received(:new)
end
end
RSpec has a very capable stubbing and mocking library built in (rspec mocks).
require 'spec_helper'
module Importer
class Partner
def self.get_details(product_code)
"original return value"
end
end
end
class FooService
def self.run
Importer::Partner.get_details('bar')
end
end
RSpec.describe FooService do
let(:partner_double) { class_double("Importer::Partner") }
before do
stub_const("Importer::Partner", partner_double)
allow(partner_double).to receive(:get_details).and_return 'our mocked value'
end
it "creates a double for the dependency" do
expect(FooService.run).to eq 'our mocked value'
end
end
class_double creates a double for the class and you can set the return values by using .expect and .allow and the mocking interface. This is quite useful since you can stub out the new or intialize methods to return a double or spy.
stub_constant will reset the constant to its previous value when the spec is done.
That said you can avoid the use of stub_constant by using constructor injection in your services:
class PhotoImportService
attr_accessor :client, :username
def initialize(username, api_client: nil)
#username = username
#client = api_client || APIClient.new(ENV.fetch('API_KEY'))
end
def run
client.get_photos(username)
end
end
I have a problem with testing one of my workers in rails app. It looks like this:
class UserStatisticsWorker
include Sidekiq::Worker
include Sidetiq::Schedulable
def perform(administration_id = nil)
administrations(administration_id).find_each do |administration|
User::StatisticsCalculator.new.recalculate_if_needed(administration.id)
end
end
private
def administrations(administration_id = nil)
administration_id.present? ? Administration.where(id: administration_id) : Administration.all
end
end
And it is tested with rspec:
require 'spec_helper'
describe UserStatisticsWorker do
describe 'perform' do
let!(:administration) { create(:administration) }
let!(:administration_2) { create(:administration) }
context 'when administration_id is present' do
it 'runs User::StatisticsCalculator for one administration' do
expect_any_instance_of(User::StatisticsCalculator).to receive(:recalculate_if_needed).once
subject.perform(administration.id)
end
end
context 'when administration_id is not present' do
it 'runs User::StatisticsCalculator for all administrations' do
expect_any_instance_of(User::StatisticsCalculator).to receive(:recalculate_if_needed).twice
subject.perform
end
end
end
end
The second spec is not pass with following error:
The message 'recalculate_if_needed' was received by #<User::StatisticsCalculator:85721520 > but has already been received by #<User::StatisticsCalculator:0x0000000a383498>
Why is that?
A very good practice is to avoid any_instance_of and instead extract private methods in your worker which can be more easily tested. A refactor would look something like this:
class UserStatisticsWorker
include Sidekiq::Worker
include Sidetiq::Schedulable
def perform(administration_id = nil)
administrations(administration_id).find_each do |administration|
recalculate_if_needed(administration)
end
end
private
def recalculate_if_needed(administration)
User::StatisticsCalculator.new.recalculate_if_needed(administration.id)
end
def administrations(administration_id = nil)
administration_id.present? ? Administration.where(id: administration_id) : Administration.all
end
end
Then, test it like this:
require 'spec_helper'
describe UserStatisticsWorker do
describe 'perform' do
let!(:administration) { create(:administration) }
let!(:other_administration) { create(:administration) }
context 'when administration_id is present' do
it 'tries to recalculate for the specific administration' do
expect(subject).to receive(:recalculate_if_needed).once
subject.perform(administration.id)
end
end
context 'when administration_id is not present' do
it 'tries to recalculate for all administrations' do
expect(subject).to receive(:recalculate_if_needed).twice
subject.perform
end
end
end
end
The problem is that you have set the expectation to happen twice on an instance... but what is actually happening is that it is being called Once on two different instances.
ie this is not the expectation that you're looking for...
see the other answer for what you could try instead.