How to write a remote request spec? - ruby-on-rails

I've got an action in my rails app that responds both in html and js format.
The result, according to the format, changes a bit. So I'd like to write one test for html and other for js response.
I'm using rspec.
Controller:
# app/controllers/my_controller.rb
class MyController < ApplicationController
def my_action
respond_to do |format|
format.html do
# Do something...
end
format.js do
# Do something else...
end
end
end
end
Spec:
# spec/requests/my_spec.rb
require "rails_helper"
RSpec.describe "My", type: :request do
describe "GET /my_action" do
context 'HTML' do
# HTML tests
end
context 'JS' do
# JS tests
end
end
end

Rails detects request format via http Accept header, in tests there's a helper for simulating xhr (also sets X-Requested-With, which is part of CSRF mitigation):
get "/some/path/to/my_action", xhr: true

Related

Rspec - Stub/allow_any_instance_of included module methods is not working

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

test update request with RSpec failed

I have two problems when I try to test the update action with RSpec, here is the controller file:
#volunteers_controller.rb
module Api
module V1
class VolunteersController < ApplicationController
before_action :find_volunteer, only: %i[show update destroy]
def update
#volunteer.update!(volunteer_params)
head :no_content
end
private
def find_volunteer
#volunteer = Volunteer.find_by!(id: params[:id])
end
def volunteer_params
params.require(:volunteer).permit(:image_url, :name, :job_desc)
end
end
end
end
Here is the test file:
require 'rails_helper'
RSpec.describe Api::V1::VolunteersController, type: :request do
...
describe '#update' do
let(:volunteer) { Volunteer.create!( :image_url=>"first.jpg", :name=>"test1", :job_desc=>"description") }
let(:params){
{:volunteer => {
"image_url"=>"new.jpg",
"name"=>"test1",
"job_desc"=>"description"
}
}
}
it 'updates a certain volunteer' do
patch :patch, :params => params #failed, bad URL
expect(volunteer.image_url).to eq("new.jpg") #failed, still return 'first.jpg'
end
it 'returns a no_content header' do
patch "http://localhost:3000/api/v1/volunteers/#{volunteer.id}", :params => params
expect(response).to have_http_status "204"
end
end
end
private
def json_parse(string)
if string.class==String
json = JSON.parse(string)
end
json
end
So my questions are:
when try to write the URL like this: patch :patch, :params => params, I got the following error:
Api::V1::VolunteersController#update updates a certain volunteer
Failure/Error: patch :patch, :params => params
URI::InvalidURIError:
bad URI(is not URI?): "http://www.example.com:80patch"
How can I change the URL to: "http://localhost:3000/api/v1/volunteers/#{volunteer.id}"?
I manually test the update action, putting a binding.pry in the update action, it does update volunteer subject, however, when it goes back to the test, it shows that it doesn't not get updated, why is that?
Thank you!!
The first problem is really your update method itself and its complete lack of error handling and meaningful feedback to the client. update! will raise ActiveRecord::RecordInvalid if the input is invalid - which is not rescued at all in your controller. And exceptions should no be used for normal code flow - invalid input is not really an exceptional event.
Instead you should rewrite your controller so that it checks if the update is performed and returns the appropriate response:
def update
if #volunteer.update(volunteer_params)
head :no_content
else
head :unprocessable_entity
end
end
As for the spec itself you're mixing up controller specs and request specs. While they look somewhat similar the key difference is that a request spec sends actual HTTP requests your rails server while a controller spec stubs the actual request and passes it to an instance of the controller under test.
In a controller spec you could write:
patch :update, params: { ... }
Because its actually calling the update method on an instance of the controller. But of course:
patch :patch, :params => params #failed, bad URL
Will not work in request spec since its not a valid URL and request specs send actual HTTP requests. Note that you should pass relative URLs and not absolute URLs as the test server may run on a different port then the dev server
# Bad
patch "http://localhost:3000/api/v1/volunteers/#{volunteer.id}", :params => params
# Good
patch "/api/v1/volunteers/#{volunteer.id}", params: params
ActiveRecord models are not "live reloading" - the representation in memory will not automatically be updated when the values in the database are updated. You need to manaully reload the record for that to happen:
it 'updates a certain volunteer' do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
volunteer.reload
expect(volunteer.image_url).to eq("new.jpg")
end
Altogether your spec should actually look something like:
# Describe the endpoint - not the controller implmentation
RSpec.describe "V1 Volunteers API", type: :request do
describe 'PATCH /api/v1/volunteers/:id' do
# use do ... end if the expression does not fit on one line
let(:volunteer) do
# enough with the hashrockets already!
Volunteer.create!(
image_url: "first.jpg",
name: "test1",
job_desc: "description"
)
end
context "with invalid parameters" do
# some set of failing parameters
let(:params) do
{
volunteer: {
name: ""
}
}
end
it "returns unproccessable entity" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect(resonse).to have_http_status :unproccessable_entity
end
it "does not update the volunteer" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect { volunteer.reload }.to_not change(volunteer, :name).to("")
end
end
context "with valid parameters" do
# some set of failing parameters
let(:params) do
{
volunteer: {
image_url: "new.jpg",
name: "test1",
job_desc: "description"
}
}
end
it "returns no content" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect(resonse).to have_http_status :no_content
end
it "updates the volunteer" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect { volunteer.reload }.to change(volunteer, :image_url)
.to("new.jpg")
end
end
end
end

RSpec: Stub controller method in request spec

I'm writing an RSpec request spec, which looks roughly like (somewhat shortened for brevity):
describe 'Items', type: :request do
describe 'GET /items' do
before do
allow_any_instance_of(ItemsController).to receive(:current_user).and_return(user)
get '/items'
#parsed_body = JSON.parse(response.body)
end
it 'includes all of the items' do
expect(#parsed_body).to include(item_1)
expect(#parsed_body).to include(item_2)
end
end
end
The controller looks like:
class ItemsController < ApplicationController
before_action :doorkeeper_authorize!
def index
render(json: current_user.items)
end
end
As you can see, I'm trying to stub doorkeeper's current_user method.
The tests currently pass and the controller works as expected. My question is about the line:
allow_any_instance_of(ItemsController).to receive(:current_user).and_return(user)
I wrote this line based on the answers in How to stub ApplicationController method in request spec, and it works. However, the RSpec docs call it a "code smell" and rubocop-rspec complains, "RSpec/AnyInstance: Avoid stubbing using allow_any_instance_of".
One alternative would be to get a reference to the controller and use instance_double(), but I'm not sure how to get a reference to the controller from a request spec.
How should I write this test avoid code smells / legacy testing approaches?
You're supposed to be on vacation.
I think the right way is to avoid stubbing as much as you can in a request spec, doorkeeper needs a token to authorize so I'd do something like:
describe 'Items', type: :request do
describe 'GET /items' do
let(:application) { FactoryBot.create :oauth_application }
let(:user) { FactoryBot.create :user }
let(:token) { FactoryBot.create :access_token, application: application, resource_owner_id: user.id }
before do
get '/items', access_token: token.token
#parsed_body = JSON.parse(response.body)
end
it 'includes all of the items' do
expect(#parsed_body).to include(item_1)
expect(#parsed_body).to include(item_2)
end
end
end
Here are some examples of what those factories might look like.
Lastly, nice SO points!
have you thought not to mock current_user at all?
if you write a test helper to sign in a user before your request spec, current_user will be populate automatically as if it was a real user. The code would look like this:
before do
sign_in user
get '/items'
#parsed_body = JSON.parse(response.body)
end
if you are using devise gem for authentication it has a nice written wiki page about that here.
This approach is also recommended here by #dhh

Rspec mysteriously passes when testing XML or CSV output

In my Rails 5 app I have this:
class InvoicesController < ApplicationController
def index
#invoices = current_account.invoices
respond_to do |format|
format.csv do
invoices_file(:csv)
end
format.xml do
invoices_file(:xml)
end
end
end
private
def invoices_file(type)
headers['Content-Disposition'] = "inline; filename=\"invoices.#{type.to_s}\""
end
end
describe InvoicesController, :type => :controller do
it "renders a csv attachment" do
get :index, :params => {:format => :csv}
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(response).to render_template :index
end
end
My problem is that my Spec always passes (!), even when I put a bunch of crap into my index.csv.erb file. It seems that the view file isn't even evaluated / tested by RSpec.
How is this possible? What am I missing here?
Controller tests/specs are these weird stubbed creations born out of the idea of unit testing controllers in isolation. That idea turned out to be pretty flawed and has really fallen out of vogue lately.
Controller specs don't actually make a real HTTP request to your application that passes through the routes. Rather they just kind of fake it and pass a fake request through.
To make the tests faster they also don't really render the views either. Thats why it does not error out as you have expected. And the response is not really a real rack response object either.
You can make RSpec render the views with render_views.
describe InvoicesController, :type => :controller do
render_views
it "renders a csv attachment" do
get :index, format: :csv
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(response).to render_template :index
end
end
But a better and more future proof option is using a request spec.
The official recommendation of the Rails team and the RSpec core team
is to write request specs instead. Request specs allow you to focus on
a single controller action, but unlike controller tests involve the
router, the middleware stack, and both rack requests and responses.
This adds realism to the test that you are writing, and helps avoid
many of the issues that are common in controller specs.
http://rspec.info/blog/2016/07/rspec-3-5-has-been-released/
# spec/requests/invoices
require 'rails_helper'
require 'csv'
RSpec.describe "Invoices", type: :request do
let(:csv) { response.body.parse_csv }
# Group by the route
describe "GET /invoices" do
it "renders a csv attachment" do
get invoices_path, format: :csv
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(csv).to eq ["foo", "bar"] # just an example
end
end
end
The format option should be specified outside of the params, i.e. get :index, params: {}, format: :csv}.
Regarding RSpec evaluating views, no, in controller tests, it doesn't, regardless of the format. However, it's possible to test views with RSpec: https://relishapp.com/rspec/rspec-rails/v/2-0/docs/view-specs/view-spec

RSpec request spec test always returning empty response

I'm currently putting together a project on Rails (4.1.8) and am trying out writing RSpec request specs to test the api routes. Strangely however it also seems to return a empty response when running the test, however doing a direct request with Postman returns the json data as expected.
Request Spec:
# spec/requests/tasks_spec.rb
require 'rails_helper'
RSpec.describe 'Tasks API', type: :request do
render_views
describe 'GET /tasks' do
it 'returns tasks' do
headers = {
"ACCEPT" => "application/json"
}
get '/tasks', headers
puts "Response: #{response}"
expect(response.content_type).to eq("application/json")
expect(json).not_to be_empty
end
end
end
Result:
Failure/Error: expect(json).not_to be_empty
expected `[].empty?` to return false, got true
The Tasks Controller is setup as follows:
class TasksController < ApplicationController
before_action :set_task, only: [:show, :update, :destroy]
# GET /tasks
def index
#tasks = Task.all
json_response(#tasks)
end
Response Concern:
module Response
def json_response(object, status = :ok)
render json: object, status: status
end
end
Spec Support:
module RequestSpecHelper
# Parse JSON response to ruby hash
def json
JSON.parse(response.body)
end
end
I had read similar questions which talk about a rende_views fix - however I've added that with no effect and also I thought you didn't need these when using Request specs as they should run through the full stack? Can anyone shed some light, quite new to request specs and a bit puzzled.
There is no tasks in your DB, that's why it is returning an empty array.
You need to create a task before calling index action.
Either you can create a factory using the facatory_girl_rails gem for Task or you can call the create action of TasksController before calling index action:
# spec/requests/tasks_spec.rb
require 'rails_helper'
RSpec.describe 'Tasks API', type: :request do
render_views
describe 'GET /tasks' do
before do
FactoryGirl.create(:task)
end
it 'returns tasks' do
headers = {
"ACCEPT" => "application/json"
}
get '/tasks', headers
puts "Response: #{response}"
expect(response.content_type).to eq("application/json")
expect(json).not_to be_empty
end
end
end
Reference FactoryGirl(Defining factories):
https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md#defining-factories

Resources