Rspec test of the object method call in controller - ruby-on-rails

I want to test that a controller create and update actions call a process method on the MentionService instance.
In controller I have MentionService.new(post).process
This is my current spec:
it "calls MentionService if a post has been submitted" do
post = Post.new(new_post_params)
mention_service = MentionService.new(post)
expect(mention_service).to receive(:process)
xhr(:post, :create, company_id: company.id, remark: new_post_params)
end
In the controller actions I have:
def create
...
# after save
MentionService.new(#remark).process
...
end
What I get is:
expected: 1 time with any arguments
received: 0 times with any arguments
Any ideas?
Thanks.

The problem is that you're creating a new instance in your test and expect that instance to receive :process which will not work.
Try playing around with this snippet:
let(:service) { double(:service) }
it "calls MentionService if a post has been submitted" do
expect(MentionService).to receive(:new).with(post).and_return(service)
expect(service).to receive(:process)
xhr(:post, :create, company_id: company.id, remark: new_post_params)
end
You need to tell your MentionService class to receive :new and return a mock object, which will receive :process. If that is the case, you know the call sequence succeeded.

If you're not interested with supplying the mock object yourself you can also modify your expectation to:
expect_any_instance_of(MentionService).to receive(:process)

Related

How to properly mock an instance of an Active Record model to test that a method has been called

With the following controller action:
def create
my_foo = MyFoo.find params[:foo_id]
my_bar = my_foo.my_bars.create! my_bar_params
my_bar.send_notifications
redirect_to my_bar
end
In my test, I am trying to assert that the method send_notifications) is called in my_bar, which is an instance of an AR model.
One way to test this would be to ensure that the notifications are sent (expect { request}.to enqueue_mail ...). But this is probably not a good practice because it pierces through abstraction layers.
Another option would be to use expect_any_instance_of:
it 'sends the notifications' do
expect_any_instance_of(MyBar).to receive :send_notifications
post my_bars_path, params: { my_bar: valid_attributes }
end
I like this method because it's clean and straightforward, but it seems that the creators of RSpec deprecated it.
The other method I tried requires mocking many AR methods:
it 'sends the notifications' do
my_bar = instance_double MyBar
allow(MyBar).to receive(:new).and_return my_bar
allow(my_bar).to receive :_has_attribute?
allow(my_bar).to receive :_write_attribute
allow(my_bar).to receive :save!
allow(my_bar).to receive :new_record?
expect(my_bar).to receive :send_notifications
allow(my_bar).to receive(:to_model).and_return my_bar
allow(my_bar).to receive(:persisted?).and_return true
allow(my_bar).to receive(:model_name).and_return ActiveModel::Name.new MyBar
post my_bars_path, params: { my_bar: valid_attributes }
end
The allows over the expect are there to mock the line #my_bar = #my_foo.my_bars.create! my_bar_params. The rest of the allows under the expect are there to mock redirect_to #my_bar.
I don’t know if this is what the creators of RSpec want us to write, but it does not seem very ergonomic.
So, my question is: is there any other way to write a test like this that does not involve mocking lots of AR internals and does not require me to change the code in my controller action?
I like [using expect_any_instance_of] because it's clean and straightforward, but it seems that the creators of RSpec deprecated it.
They discourage it with good reason. What if the controller code changes and something else calls send_notifications? The test will pass.
Having to use expect_any_instance_of or allow_any_instance_of indicates the code is doing too much and can be redesigned.
What can't be solved by adding another layer of abstraction?
def create
my_bar.send_notifications
redirect_to my_bar
end
private
def my_foo
MyFoo.find params[:foo_id]
end
def my_bar
my_foo.my_bars.create! my_bar_params
end
Now you can mock my_bar to return a double. If the method made more extensive use of my_bar, you can also return a real MyBar.
it 'sends the notifications' do
bar = instance_double(MyBar)
allow(#controller).to receive(:my_bar).and_return(bar)
post my_bars_path, params: { my_bar: valid_attributes }
end
Encapsulating finding and creating models and records within a controller is a common pattern.
It also creates a pleasing symmetry between the test and the method which indicates the method is doing exactly as much as it has to and no more.
Or, use a service object to handle the notification and check that.
class MyNotifier
def self.send(message)
...
end
end
class MyBar
NOTIFICATION_MESSAGE = "A bar, a bar, dancing in the night".freeze
def send_notification
MyNotifier.send(NOTIFICATION_MESSAGE)
end
end
Then test the notification happens.
it 'sends the notifications' do
expect(MyNotifier).to receive(:send)
.with(MyBar.const_get(:NOTIFICATION_MESSAGE))
post my_bars_path, params: { my_bar: valid_attributes }
end
By making send a class method, we don't need to use expect_any_instance_of. Writing services objects as singleton classes which have no state is a common pattern for this reason and many others.
The downside here is it does require knowledge of how MyBar#send_notification works, but if the app uses the same service object to do notifications this is acceptable.
Or, create a MyFoo for it to find. Mock its call to create MyBar being sure to check the arguments are correct.
let(:foo) {
MyFoo.create!(...)
}
let(:foo_id) { foo.id }
it 'sends the notifications' do
bar = instance_double(MyBar)
expect(bar).to receive(:send_notifications).with(no_args)
allow(foo).to receive_message_chain(:my_bars, :create!)
.with(valid_attributes)
.and_return(bar)
post my_bars_path, params: { foo_id: foo_id, my_bar: valid_attributes }
end
This requires more knowledge of the internals, and rubocop-rspec does not like message chains.
Or, mock MyFoo.find to return a MyFoo double. Again, be sure to only accept the proper arguments.
it 'sends the notifications' do
foo = instance_double(MyFoo)
allow(foo).to receive_message_chain(:my_bars, :create!)
.with(valid_attributes)
.and_return(bar)
bar = instance_double(MyBar)
expect(bar).to receive(:send_notifications).with(no_args)
allow(MyFoo).to receive(:find).with(foo.id).and_return(foo)
post my_bars_path, params: { foo_id: foo_id, my_bar: valid_attributes }
end
Alternatively you could allow(MyFoo).to receive(:find).with(foo_id).and_return(mocked_foo), but I find it's better to mock as little as possible.
What I'd do in this case is to make the controller "dumb", move the persistence and notification logic into a service, and test the service in integration instead of stubbing.
# controller
def create
my_bar = CreateBar.call params[:foo_id], my_bar_params
redirect_to my_bars_path(my_bar)
end
# service
class CreateBar
def self.call(foo_id, bar_params)
foo = MyFoo.find params[:foo_id]
bar = foo.my_bars.create! bar_params
bar.send_notifications
bar
end
end
# controller spec
it 'creates a new bar' do
bar = double :bar, id: 'whatever'
expect(CreateBar).to receive(:call).with(<attributes>).and_return(bar)
post my_bars_path, params: {my_bar: valid_attribute}
end
it 'redirects to the created bar' do
bar = double :bar, id: 1
allow(CreateBar).to receive(:call).with(<attributes>).and_return(bar)
post my_bars_path, params: {my_bar: valid_attribute}
expect(response).to redirect_to(my_bars_path(1))
end
# service spec
it 'creates a bar' do
# Or just create the object via AR's interface if you don't use factory bot.
foo = FactoryBot.create :foo
CreateBar.call foo.id, <bar_params>
bars = Foo.find(foo.id).bars
expect(bars.count).to eq 1
expect(bars.first).to have_attribute <expected_attributes>
end
it 'sends a bar notification' do
foo = FactoryBot.create :foo
CreateBar.call foo.id, <bar_params>
# NotificationCenter is expected to be called from #send_notification
# and in test environment it records your notifications instead of actually
# sending them.
notifications = NotificationCenter.notifications
expect(notifications.count).to eq <expected notifications count>
# + assert that the notifications have the properties based on the <bar_params>
end
Depending on the complexity of #send_notifications, you can either inline it in CreateFoo (thus keeping the persistence model separate from the business logic) or have a separate service SendFooNotifications.

How do you cause an expectation to halt execution in RSpec?

I want to test that a class receives a class-method call in RSpec:
describe MyObject do
it "should create a new user" do
expect(User).to receive(:new)
MyObject.new.doit
end
end
class MyObject
def doit
u = User.new
u.save
end
end
The problem is that the expectation does not halt execution. It simply stubs the class method .doit and continues execution.
The effect of the expectation is to ensure that User.new returns nil. So when we get to the next line which is User.save it then fails because there is no user object to call .save on.
I would like execution to halt as soon as the RSpec expectation has been satisfied - how can I do that?
nb
This is just an illustrative example - while an expect to change would work for User.new, it's not this actual code that I need to test
There is a great method for this and_call_original:
expect(User).to receive(:new).and_call_original
based on your test description, you're testing that a record was created, in those cases I would suggest you to do this:
expect {
MyObject.new.doit
}.to change{User.count}
or if you want to make sure it only created one:
expect {
MyObject.new.doit
}.to change{User.count}.by(1)

Rails testing controller private method with params

I have a private method in a controller
private
def body_builder
review_queue = ReviewQueueApplication.where(id: params[:review_queue_id]).first
...
...
end
I would like to test just the body_builder method, it is a method buidling the payload for an rest client api call. It needs access to the params however.
describe ReviewQueueApplicationsController, type: :controller do
describe "when calling the post_review action" do
it "should have the correct payload setup" do
#review_queue_application = ReviewQueueApplication.create!(application_id: 1)
params = ActionController::Parameters.new({ review_queue_id: #review_queue_application.id })
expect(controller.send(:body_builder)).to eq(nil)
end
end
end
If I run the above it will send the body_builder method but then it will break because the params have not been set up correctly as they would be in a call to the action.
I could always create a conditional parameter for the body_builder method so that it either takes an argument or it will use the params like this def body_builder(review_queue_id = params[:review_queue_id]) and then in the test controller.send(:body_builder, params), but I feel that changing the code to make the test pass is wrong it should just test it as it is.
How can I get params into the controller before I send the private method to it?
I think you should be able to replace
params = ActionController::Parameters.new({ review_queue_id: #review_queue_application.id })
with
controller.params = ActionController::Parameters.new({ review_queue_id: #review_queue_application.id })
and you should be good. The params is just an attribute of the controller (the actual attribute is #_params but there are methods for accessing that ivar. Try putting controller.inspect in a view).

How to test a rails PORO called from controller

I have extracted part of my Foos controller into a new rails model to perform the action:
foos_controller.rb
class FoosController < ApplicationController
respond_to :js
def create
#foo = current_user.do_something(#bar)
actioned_bar = ActionedBar.new(#bar)
actioned_bar.create
respond_with #bar
end
actioned_bar.rb
class ActionedBar
def initialize(bar)
#bar = bar
end
def create
if #bar.check?
# do something
end
end
end
I got it working first but now I'm trying to back-fill the rspec controller tests.
I'll be testing the various model methods and will be doing a feature test to make sure it's ok from that point of view but I would like to add a test to make sure the new actioned_bar model is called from the foos controller with #bar.
I know in rspec you can test that something receives something with some arguments but I'm struggling to get this to work.
it "calls ActionedBar.new(bar)" do
bar = create(:bar)
expect(ActionedBar).to receive(:new)
xhr :post, :create, bar_id: bar.id
end
This doesn't work though, the console reports:
NoMethodError:
undefined method `create' for nil:NilClass
which is strange because it only does this when I use expect(ActionedBar).to receive(:new), the rest of the controller tests work fine.
If I try to do:
it "calls ActionedBar.new(bar)" do
bar = create(:bar)
actioned_bar = ActionedBar.new(bar)
expect(actioned_bar).to receive(:create).with(no_args)
xhr :post, :create, bar_id: bar.id
end
the console says:
(#<ActionedBar:0xc8f9f74>).create(no args)
expected: 1 time with no arguments
received: 0 times with no arguments
If I do a put in the controller whilst running the test; for some reason this test causes the actioned_bar in the controller to be output as nil but fine for all the other controller tests.
Is there any way I can test that ActionedBar is being called in this controller spec?
You can use expect_any_instance_of(ActionedBar).to receive(:create), because instance in spec and in controller are different instances.
If you want to use original object, you can use expect(ActionedBar).to receive(:new).and_call_original (without that #new just will return nil and you'll get NoMethodError).
You can set up a double ActionedBar which is returned by the ActionedBar.new call as this instance is different to the one used in the controller.
describe "#create" do
let(:actioned_bar) { double(ActionedBar) }
let(:bar) { double(Bar) }
it "calls ActionedBar.new(bar)" do
expect(ActionedBar).to receive(:new).with(bar).and_returns(actioned_bar)
expect(actioned_bar).to receive(:create)
xhr :post, :create, bar_id: bar.id
end
end
The core problem is that actioned_bar in your spec in is not going to be the the same instance of ActionedBar that is in your controller. Thus the spec will always fail.
Instead you need to have ActionedBar return a double when new is called:
it "calls ActionedBar.new(bar)" do
bar = create(:bar)
actioned_bar = instance_double("ActionedBar")
allow(ActionedBar).to receive(:new).and_return(actioned_bar)
expect(actioned_bar).to receive(:create).with(no_args)
xhr :post, :create, bar_id: bar.id
end
However I generally consider this kind of test a code smell - its ok to mock out external collaborators and set expectations that you are passing the correct messages. But your might want to consider if your are testing the details of how your controller does its job and not the actual behavior.
I find it better to setup a spec which calls the controller action and set expectations on what how it for example changes the database state or how it effects the response.

Expect to receive working for any_instance but not for specific one

I have the following rspec:
context 'when there is an incoming error' do
it 'should update status of url to error' do
url = create(:url)
error_params = {error: 'whatever', url_id : url.id}
expect(url).to receive(:set_error)
post :results, error_params
end
end
And the results action looks like this:
def results
url = Url.find(url_id: params['url_id'])
if params.key?('error') && !params['error'].blank?
url.set_error
end
end
If I do it like this, the test does not pass:
expected: 1 time with any arguments
received: 0 times with any arguments
However, if I change to:
expect_any_instance_of(Url).to receive(:set_error).
It passes. I just have one Url, so I am not sure what is going on.
When you create a to receive expectation, it is connected to a specific Ruby object.
When the results action is called, it instantiates a new url object. It represents the same database object that you called your expectation on in the Rspec example. But it isn't the same Ruby object - it's a new object with (probably) the same data. So the expectation fails.
To illustrate:
describe ".to_receive" do
it "works on Ruby objects" do
url = Url.create(:url)
same_url = Url.find(url.id)
expect(url).to_not receive(:to_s)
same_url.to_s
end
end
To (somewhat) get the desired behaviour you could use any_instance and change the controller so that it assigns the url object to an instance variable. In that way you can inspect the url object easier:
# Change your action so that it saves the url object as an instance variable
def results
#url = Url.find(url_id: params['url_id'])
if params[:error].present?
#url.set_error
end
end
# Change your spec to look at the assigned variable
context 'when there is an incoming error' do
it 'should update status of url to error' do
url = create(:url)
error_params = {error: 'whatever', url_id: url.id}
expect_any_instance_of(Url).to receive(:set_error)
post :results, error_params
expect(assigns(:url)).to eq(url)
end
end
Since assigns only lets you inspect the assigned ivar after the controller action has been executed, you can't use it to create a receive expectation.

Resources