RSpec Delayed Expectation Argument Matchers - ruby-on-rails

I currently have some rails code in a controller that does something like this:
def update
#local.update(...)
#local.save!
Exchange::publish_to_exchange('event-title', Serializer.new(#local).serializable_hash)
render ...
end
I want to test that the message has been published with the correct message name and the serialized, updated object. So, in the controller spec I do something like this:
let(:local) { create(:local_class) }
it 'publishes serialized version to exchange' do
expect(Exchange).to receive(:publish_to_exchange).with('event-title', Serializer.new(local).serializable_hash)
put :update, params
end
However, this obviously doesn't work because it is comparing the unupdated, serializable hash of local with the updated version. I'm wondering if there is anyway to delay the checking of the matcher after the "put :update, params," line, so that I can test this case.

I want to test that the message has been published with the correct message name and the serialized, updated object...
That is not really appropriate for a controller test, since the publishing occurs outside of scope of the controller action. If you need to write a unit test for it, you are much better off putting that test in the model, or in a separate unit (like a PORO loaded from lib).

Related

Rspec: Test if instance method was called on specific record with ActiveRecord

I'm looking to write a controller spec which tests if a method was called on an instance of a model class for ActiveRecord.
For example, there is a model Post and I want to check if the post with the id 55 had the method foobar called on it.
Some ways that almost work:
expect_any_instance_of(Post).to receive(:foobar)
This almost works but it can not check which post the method was called on
using double
This would normally work but in the controller spec, only ids are passed over so I have no way of inserting the double, short of mocking the response from activerecord find
Does rspec provide any tools to check a method was called on a specific model instance?
It is possible but requires much more mocking, and this means that you're testing the implementation details of a controller which is an antipattern.
Assuming your controller is this:
def action(id)
Post.find(params[:id]).foobar
end
You'd need to mock sth like this (I'll ignore RSpec's good practices like having vars in let's, you can easily extract it later when you have a working example):
mock = instance_double(Post)
expect(Post).to receive(:find).with(55).and_return(mock)
expect(mock).to receive(:foobar)
# trigger the action here
But as the comments already stated, it's probably cleaner to test side effects. If your action deletes a Post, check that the post has been deleted
expect { trigger_action params: {id: 55} }.to change(Post, :count).by(-1)
This makes your code less brittle (you can refactor the internals, and the specs for the controller stay green - meaning your app still works as described in those specs).

How to write a good request spec for #index?

I am new to TDD and really enjoy it. I am using RSpec.
I am trying to learn to write good request specs (in general) and can find very little written on how to test the index method.
I have found this article: https://medium.com/#lcriswell/rails-api-request-specs-with-rspec-effeac468c4e, but I am not interested in testing an API, but an application with views.
What should I include on my index request tests and why?
The first spec in the article is great, you can use it in testing a regular controller for responses.
If you are using any sort of authorization(e.g cancancan), you can test the same request for multiple types of users and check if you get a redirect or a success(you might have to mock the sign-in).
For testing the views that are being rendered, you can try this:
it { is_expected.to render_template(:index) }
If your action assigns instance variables, you can test out that the variable is a certain value like so:
expect(assigns(:foo)).to be true
If your action responds to different formats(HTML, json, ...), you can write different contexts for each of the formats, each time by changing the request(hint: for JS in your specs submit your request like so: get :index, xhr: true)

Rspec: How to use expect to receive with a resource which does not exist yet?

In an action called via a post request I'm creating a resource call RequestOffer and send an email with ActionMailer using the created resource as a parameter:
#request_offer = RequestOffer.new(request_offer_params)
if #request_offer.save
RequestOfferMailer.email_team(#request_offer).deliver_later
end
When my controller spec, I want to test that my RequestOfferMailer is called using the method email_team with the resource #request_offer as a parameter.
When I want to user expect(XXX).to receive(YYY).with(ZZZ), the only way I found was to declare my expectation before making the POST request. However, ZZZ is created by this POST request, so I have no way to set my expectation before.
# Set expectation first
message_delivery = instance_double(ActionMailer::MessageDelivery)
# ZZZ used in .with() does not exist yet, so it won't work
expect(RequestOfferMailer).to receive(:email_team).with(ZZZ).and_return(message_delivery)
expect(message_delivery).to receive(:deliver_later)
# Make POST request that will create ZZZ
post :create, params
Any idea how to solve this problem?
If this is a functional test then I would isolate the controller test from the DB. You can do this by using instance_doubles and let statements. Here's an example that you may like to extend for your purposes
describe '/request_offers [POST]' do
let(:request_offer) { instance_double(RequestOffer, save: true) }
before do
allow(RequestOffer).to receive(:new).
with(...params...).
and_return(request_offer)
end
it 'should instantiate a RequestOffer with the params' do
expect(RequestOffer).to receive(:new).
with(...params...).
and_return(request_offer)
post '/request_offers', {...}
end
it 'should email the request offer via RequestOfferMailer' do
mailer = instance_double(ActionMailer::MessageDelivery)
expect(RequestOfferMailer).to receive(:email_team).
with(request_offer).and_return(mailer)
post '/request_offers', {...}
end
end
The key to this is using 'let' to declare an instance double of the model that you intend to create. By setting expectations on the class you can inject your instance double into the test and isolate from the DB. Note that the 'allow' call in the before block is there to serve the later specs that set expectations on the mailer object; the 'expect' call in the first test will still be able to make assertions about the call.
Would it be enough to make sure the argument is an instance of RequestOffer? Then you could use the instance_of matcher. For example:
expect(RequestOfferMailer).to receive(:email_team).with(instance_of(RequestOffer)).and_return(message_delivery)
I found this option in the Rspec 3.0 docs: https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/setting-constraints/matching-arguments
The last argument of the with method is a block. You can open up the arguments and do anything you like there.
expect(RequestOfferMailer)
.to receive(:email_team)
.with(instance_of(RequestOffer)) do |request_offer|
expect(request_offer.total).to eq(100) # As one example of what you can to in this block
end.and_return(message_delivery)
You can also set the instance_of matcher to be anything if you're not even sure what object type you're expecting.

mocking controller methods in rspec

(This question is similar to Ruby on Rails Method Mocks in the Controller, but that was using the old stub syntax, and besides, that didn't receive a working answer.)
short form
I want to test my controller code separate from my model code. Shouldn't the rspec code:
expect(real_time_device).to receive(:sync_readings)
verify that RealTimeDevice#sync_readings gets called, but inhibit the actual call?
details
My controller has a #refresh method that calls RealTimeDevice#sync_readings:
# app/controllers/real_time_devices_controller.rb
class RealTimeDevicesController < ApplicationController
before_action :set_real_time_device, only: [:show, :refresh]
<snip>
def refresh
#real_time_device.sync_readings
redirect_to :back
end
<snip>
end
In my controller tests, I want to verify that (a) that #real_time_device is being set up and (b) the #sync_reading model method is getting called (but I don't want to invoke the model method itself since that's covered by the model unit tests).
Here's my controller_spec code that doesn't work:
# file: spec/controllers/real_time_devices_controller_spec.rb
require 'rails_helper'
<snip>
describe "PUT refresh" do
it "assigns the requested real_time_device as #real_time_device" do
real_time_device = RealTimeDevice.create! valid_attributes
expect(real_time_device).to receive(:sync_readings)
put :refresh, {:id => real_time_device.to_param}, valid_session
expect(assigns(:real_time_device)).to eq(real_time_device)
end
end
<snip>
When I run the test, the actual RealTimeDevice#sync_readings method is getting called, i.e., it's trying to call code in my model. I thought the line:
expect(real_time_device).to receive(:sync_readings)
was necessary and sufficient to stub the method and verify that it got called. My suspicion is that it needs to be a double. But I can't see how to write the test using a double either.
What am I missing?
You're setting an expectation on a specific instance of RealTimeDevice. The controller fetches the record from the database, but in your controller, it's using another instance of RealTimeDevice, not the actual object you set the expectation on.
There are two solutions to this problem.
The Quick and Dirty
You can set an expectation on any instance of RealTimeDevice:
expect_any_instance_of(RealTimeDevice).to receive(:sync_readings)
Note that this is not the best way to write your spec. After all, this doesn't guarantee that your controller fetches the right record from the database.
The Mocking Approach
The second solution involves a bit more work, but will cause your controller to be tested in isolation (which it is not really if it's fetching actual database records):
describe 'PUT refresh' do
let(:real_time_device) { instance_double(RealTimeDevice) }
it 'assigns the requested real_time_device as #real_time_device' do
expect(RealTimeDevice).to receive(:find).with('1').and_return(real_time_device)
expect(real_time_device).to receive(:sync_readings)
put :refresh, {:id => '1'}, valid_session
expect(assigns(:real_time_device)).to eq(real_time_device)
end
end
Quite some things have changed. Here's what happens:
let(:real_time_device) { instance_double(RealTimeDevice) }
Always prefer using let in your specs rather than creating local variables or instance variables. let allows you to lazy evaluate the object, it's not created before your spec requires it.
expect(RealTimeDevice).to receive(:find).with('1').and_return(real_time_device)
The database lookup has been stubbed. We're telling rSpec to make sure that the controller fetches the correct record from the database. The important part is that the very instance of the test double created in the spec is being returned here.
expect(real_time_device).to receive(:sync_readings)
Since the controller is now using the test double rather than an actual record, you can set expectations on the test double itself.
I've used rSpec 3's instance_double, which verifies the sync_readings method is actually implemented by the underlying type. This prevents specs from passing when a method would be missing. Read more about verifying doubles in the rSpec documentation.
Note that it's not required at all to use a test double over an actual ActiveRecord object, but it does make the spec much faster. The controller is now also tested in complete isolation.

Dynamic method call in routes spec

I am testing simple get requests for my routes using rspec in my Rails 3.2 application. Since all are get requests, and all just have different action names which are similar to the views' names, it would be really repetitive to manually write a different test for each get request.
Instead, I wanted to come up with something like this:
%(action_1 action_2 action_3 action_4).each do |action|
it "routes to the #{action} page" do
get("liver_diseases#{action}_path").should route_to("liver_diseases##{action}")
end
end
It fails at this pseudocode: get("liver_diseases_#{action}_path")
So what I need to do is a dynamic method call - but for what I have found out, that would involve .send(:method_name), for which I need to know the class name. And I couldn't find that.
What do I need to do for this method call to work?
that would involve .send(:method_name), for which I need to know the
class name
When the receiver is missing, it's always self. In the context of a controller example, self should be a controller instance. So you should be able to get that path with:
send "liver_diseases_#{action}_path"
which should be equivalent to:
controller.send "liver_diseases_#{action}_path"

Resources