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.
Related
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.
I am trying to write two RSpec tests for two different problems that are much more advanced that what I'm used to writing.
What I'm trying to test within my controller:
def index
#buildings ||= building_class.active.where(place: current_place)
end
My attempt at writing the RSpec test:
describe 'GET :index' do
it "assigns #buildings" do
#buildings ||= building_class.active.where(place: current_place)
get :index
expect(assigns(:buildings)).to eq([building])
end
end
This test failed and wouldn't even run so I know I'm missing something.
My second test is needing to test the returned value of a class method. Here is what I am needing to test within the controller:
def class_name
ABC::Accountant::Business
end
Here is my attempt at testing this method:
describe "class name returns ABC::Accountant::Business" do
subject do
expect(subject.class_name).to eq(ABC::Accountant::Business)
end
end
For the first test I would do something like this:
First, I would move that .active.where(place: current_place) to a scope (I'm guessing building_class returns Building or something like that):
class Building << ApplicationRecord
scope :active_in, -> (place) { active.where(place: place)
Then it's easier to stub for the test
describe 'GET :index' do
it "assigns #buildings" do
scoped_buildings = double(:buildings)
expect(Building).to receive(:active_in).and_return(scoped_buildings)
get :index
expect(assigns(:buildings)).to eq(scoped_buildings)
end
end
Then your controller will do
#buildings ||= building_class.active_in(current_place)
This way you are testing two things: that the controller actually calls the scope and that the controller assigns the returned value on the #buildings variable (you don't really need to test the actual buidlings, you can test the scope on the model spec).
Personally, I feel like it would be better to do something like #buildings = current_place.active_buildings using the same idea of the scope to test that you are getting the active buildings of the current place.
EDIT: if you can't modify your controller, then the stubbing is a little different and it implies some chaining of methods that I don't like to explicitly test.
scoped_buildings = double(:buildings)
controller.stub_chain(:building_class, :active, :where).and_return(scoped_building)
get :index
expect(assings(:buildings)).to eq scoped_buildings
Note that now your test depends on a specific implementation and testing implementation is a bad practice, one should test behaviour and not implementation.
For the second, I guess something like this should work:
describe ".class_name" do
it "returns ABC::Accountant::Business" do
expect(controller.class_name).to eq(ABC::Accountant::Business)
end
end
IMHO, that the method's name if confusing, class_name gives the idea that it returns a string, you are not returnin a name, you are returning a class. Maybe you can change that method to resource_class or something less confusing.
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)
Let's say I have a dummy presenter class for my dummy model like this:
class DummyPresenter
def initialize(dummy_id)
#dummy = DummyModel.find(dummy_id)
end
def id
#dummy.id
end
def change_child_dummy_name(child_dummy_id, new_child_dummy_name)
child_dummy = #dummy.child_dummies.find(child_dummy_id)
child_dummy.update_attributes(:display_name => new_child_dummy_name)
child_dummy # I need to return a child_dummy object here!!
end
end
In my spec:
require 'spec_helper'
describe DummyPresenter do
before :all do
#dummy_presenter = DummyPresenter.new(1)
#child_dummy = DummyModel.find(1).child_dummies.first
end
it 'should update the display name of a child dummy for a dummy' do
expect(#child_dummy.display_name).to be_nil
#dummy_presenter.change_child_dummy_name(#child_dummy.id, 'Child Dummy network')
#child_dummy.reload
expect(#child_dummy.display_name).to eq('Child Dummy network')
end
it 'should return updated child dummy' do
child_dummy_id = #child_dummy.id
#dummy_presenter.should_receive(:change_child_dummy_name).at_least(:once).with(child_dummy_id, 'Child Dummy network').and_return(#child_dummy)
#dummy_presenter.change_child_dummy_name(child_dummy_id, 'Child Dummy network')
end
end
Above test cases pass without any issue.
Now, as per my understanding the first it block works perfectly fine where I just see the updated attribute. But, the second block where I expect the method: change_child_dummy_name to return #child_dummy doesn't work or maybe I didn't understand the code I've written here properly. Because, when I change change_child_dummy_name method inside presenter to this:
def change_child_dummy_name(child_dummy_id, new_child_dummy_name)
child_dummy = #dummy.child_dummies.find(child_dummy_id)
child_dummy.update_attributes(:display_name => new_child_dummy_name)
"child_dummy" # A String!! Where as I need to return a child_dummy object here!!
end
The specs again pass without raising any error. So, what am I doing wrong?
If I am not mistaken, the essence of this question is here
#dummy_presenter.should_receive(:change_child_dummy_name).at_least(:once).with(child_dummy_id, 'Child Dummy network').and_return(#child_dummy)
should_receive actually stubs the method's result.
if and_returns is used, its operand is the new value, if not the stubbed value is nil.
In your case that is the #child_dummy object. Which by the way is the reason your test passed the first time as well!
One way to bypass this behavior is to use .and_call_original which will do what you expect.
You should rewrite it as two tests:
one that tests that change_child_dummy_name is called (maybe it is not necessairy)
one that will test that the desired attributes of #child_dummy (because the object you create in your rspec test will not be the same that the method will return).
I have this class:
class EnablePost
def initialize(post_klass, id)
raise "oops" if post_klass.blank?
#post_klass = post_klass
#id = id
end
def perform
post = #post_klass.find_by_id(#id)
return unless post
post.update_attribute :enabled, true
end
end
The spec I have to write to test the above:
describe EnablePost do
it "should enable a post" do
post = mock
post.should_receive(:blank?).and_return(false)
post.should_receive(:find_by_id).with(22).and_return(post)
post.should_receive(:update_attribute).with(:enabled, true)
result = EnablePost.new(Post, 22).perform
result.should be_true
end
end
But what I really want to do is treat EnablePost as a black box. I don't want to have to mock :blank?, :find_by_id or :update_attribute.
That is to say I want my spec to look like:
describe EnablePost do
it "should enable a post" do
post = mock
result = EnablePost.new(post, 22).perform
result.should be_true
end
end
What am I missing here? Am I using mocks incorrectly?
Yes, you're confusing mocks and stubs.
A good mock explanation: http://jamesmead.org/talks/2007-07-09-introduction-to-mock-objects-in-ruby-at-lrug/
Mocks:
Different things to different people
Ambiguous terminology
Confusion with Rails “mocks”
Mock Object:
Expected method invocations set in advance
Verifies actual invocations match expected ones
Also check out http://martinfowler.com/articles/mocksArentStubs.html [thanks to user Zombies in the comments]
If you're using RSpec, it aliases double, mock, and stub. RSpec expects you to choose whichever method name makes your code clearest.
Your first chunk of test code is using the word "mock" correctly. You're setting up the method invocations that you expect to be called, in advance, and then performing them.
However, you're testing two different areas of your code: the first area is the initialize method, the second is the #perform method.
You may find it easier to mock and stub if you write smaller methods:
# What you want to test here is the raise and the member variables.
# You will stub the post_klass.
def initialize(post_klass, post_id) # post_id is a better name
raise "oops" if post_klass.blank?
#post_klass = post_klass
#post_id = post_id # because we don't want to mask Object#id
end
attr_accessor :post_id
attr_accessor :post_klass
# What you want to test here is the post_klass calls #find_by_id with post_id.
# See we've changed from using instance variables to methods.
def post
post_klass.find_by_id(post_id)
end
# What you want to test here is if the update happens.
# To test this, stub the #post method.
def perform
p = post
return unless p
p.update_attribute :enabled, true
end
When you write your code this way, you make it easy to stub the #post method.
See this for RSpec example source code showing the difference between mock and stub:
http://blog.firsthand.ca/2011/12/example-using-rspec-double-mock-and.html