Prevent duplicated code - ruby-on-rails

I have this test:
it "redirects him to vice" do
get :show
uri_request_1 = Addressable::URI.parse(response.redirect_url)
redirects_to_vice_1 = uri_request_1.host == "vice.com"
get :show, relative_path: "/Fun/Facts.html"
uri_request_2 = Addressable::URI.parse(response.redirect_url)
redirects_to_vice_2 = uri_request_1.host == "vice.com"
assert redirects_to_vice_1 && redirects_to_vice_2
end
It has a lot of repetition. How can I shorten this test?

I would think about adding a helper method:
def assert_redirection_to(host)
assert Addressable::URI.parse(response.redirect_url) == host
end
With such a method you could change your tests to:
describe 'without parameters' do
it 'redirects to vice.com' do
get :show
assert_redirection_to 'vice.com'
end
end
describe 'with parameters' do
it 'redirects to vice.com'
get :show, relative_path: '/Fun/Facts.html'
assert_redirection_to 'vice.com'
end
end

[[:show], [:show, relative_path: "/Fun/Facts.html"]].each do
|args|
get(*args)
assert Addressable::URI.parse(response.redirect_url).host == "vice.com"
end

Related

RSpec returns empty string in a fairly simple get request

I want to test show action in my Shipment controller. To do so I've prepared fairly simple specs:
RSpec.describe ShipmentsController, type: :controller do
describe 'GET #show' do
let(:params) { { id: shipment.id, product_id: product.id } }
let!(:product) { create(:product) }
let!(:shipment) { create(:shipment, product: product) }
context 'when params are valid' do
before { get :show, params: params }
it 'return valid json' do
expect(JSON.parse(response.body)).to eq(expected_json)
end
end
end
end
ShimpentsController.rb
class ShipmentsController < ApplicationController
before_action :set_product
attr_reader :shipment
def show
#shipment = Shipment.find(params[:id])
#items = shipment&.grouped_shipment_items
end
private
def set_product
#product = Product.find(params[:product_id])
end
end
When I use postman everything went well - it returns expected json but in the RSpec test I'm getting:
response.body
=> ""
I think you need to add render_views in your controller spec file.
RSpec.describe ShipmentsController, type: :controller do
render_views
describe 'GET #show' do
let(:params) { { id: shipment.id, product_id: product.id } }
let!(:product) { create(:product) }
let!(:shipment) { create(:shipment, product: product) }
context 'when params are valid' do
before { get :show, params: params }
it 'return valid json' do
expect(JSON.parse(response.body)).to eq(expected_json)
end
end
end
end
Reference: https://rubyinrails.com/2019/04/11/rails-test-jbuilder-json-response-with-rspec/
I think you are not making a request for JSON response with rspec. You can check by putting a breakpoint in your controller action, then checking
request.format.json?
In order to ask for JSON response from an rspec test, you should add as: :json to the end of the request. The request should look like this:
get :show, params: params, as: :json

Rspec testing attributes after creating record in controller

Background: I've got an after_action callback in my controller, which takes the string address, processes it and stores longitude and latitude in corresponding fields. I want to test this.
This SO question, as well as this article only consider update methods, but at least, they are quite clear, because I've already got an object to work with.
So my question is - how to find this newly created record? This SO question led me to this code:
require 'rails_helper'
RSpec.describe Admin::Settings::GeneralSettingsController, type: :controller do
context "POST methods" do
describe "#edit and #create" do
it "encodes and stores lang/lot correctly" do
post :create, general_setting: FactoryGirl.attributes_for(:general_setting)
expect(assigns(:general_setting).long).to eq(37.568021)
# expect(general_setting.long).to eq(37.568021)
# expect(general_setting.lat).to eq(55.805553)
end
end
end
end
But using the code in the answer, I get this error:
Failure/Error: expect(assigns(:general_setting).long).to eq(37.568021)
NoMethodError:
undefined method `long' for nil:NilClass
Update #1:
This is my new controller spec code:
RSpec.describe Admin::Settings::GeneralSettingsController, type: :controller do
context 'POST methods' do
before(:each) do
allow(subject).to receive(:set_long_lat)
end
describe 'post create' do
before(:each) do
post :create, params: { general_setting: FactoryGirl.attributes_for(:general_setting) }
end
it "saves the record with valid attributes" do
expect{subject}.to change{GeneralSetting.count}.by(1)
end
it 'calls :set_long_lat' do
expect(subject).to have_received(:set_long_lat)
end
end
end
describe '#set_long_lat' do
# spec for method
end
end
Update #2:
Here is my controller code:
class Admin::Settings::GeneralSettingsController < AdminController
include CrudConcern
before_action :find_general_setting, only: [:edit, :destroy, :update, :set_long_lat]
after_action :set_long_lat
def index
#general_settings = GeneralSetting.all
end
def new
#general_setting = GeneralSetting.new
# Билдим для того, что бы было видно сразу одно поле и пользователь не должен
# кликать на "добавить телефон"
#general_setting.phones.build
#general_setting.opening_hours.build
end
def edit
# Тоже самое, что и с нью - если телефонов нет вообще, то показываем одно пустое поле
if #general_setting.phones.blank?
#general_setting.phones.build
end
if #general_setting.opening_hours.blank?
#general_setting.opening_hours.build
end
end
def create
#general_setting = GeneralSetting.new(general_setting_params)
create_helper(#general_setting, "edit_admin_settings_general_setting_path")
end
def destroy
destroy_helper(#general_setting, "admin_settings_general_settings_path")
end
def update
# debug
# #general_setting.update(language: create_hash(params[:general_setting][:language]))
#general_setting.language = create_hash(params[:general_setting][:language])
update_helper(#general_setting, "edit_admin_settings_general_setting_path", general_setting_params)
end
private
def set_long_lat
geocoder = Geocoder.new
data = geocoder.encode!(#general_setting.address)
#general_setting.update!(long: data[0], lat: data[1])
end
def find_general_setting
#general_setting = GeneralSetting.find(params[:id])
end
def general_setting_params
params.require(:general_setting).permit(GeneralSetting.attribute_names.map(&:to_sym).push(
phones_attributes: [:id, :value, :_destroy, :general_setting_id ]).push(
opening_hours_attributes: [:id, :title, :value, :_destroy, :deneral_setting_id]) )
end
def create_hash(params)
language_hash = Hash.new
params.each do |param|
language_hash[param.to_sym] = param.to_sym
end
return language_hash
end
end
(If it helps - I've got a lot of similar crud-actions, that is why I've put them all in a concern controller)
module CrudConcern
extend ActiveSupport::Concern
include Language
included do
helper_method :create_helper, :update_helper, :destroy_helper, :get_locales
end
def get_locales
#remaining_locales = Language.get_remaining_locales
end
def create_helper(object, path)
if object.save!
respond_to do |format|
format.html {
redirect_to send(path, object)
flash[:primary] = "Well done!"
}
end
else
render :new
flash[:danger] = "Something not quite right"
end
#remaining_locales = Language.get_remaining_locales
end
def update_helper(object, path, params)
if object.update!(params)
respond_to do |format|
format.html {
redirect_to send(path, object)
flash[:primary] = "Well done!"
}
end
else
render :edit
flash[:danger] = "Something's not quite right"
end
end
def destroy_helper(object, path)
if object.destroy
respond_to do |format|
format.html {
redirect_to send(path)
flash[:primary] = "Well done"
}
end
else
render :index
flash[:danger] = "Something's not quite right"
end
end
end
Update #3
It's not the ideal solution, but, somehow, controller tests just won't work. I've moved my callback into the model and updated my general_setting_spec test.
class GeneralSetting < ApplicationRecord
after_save :set_long_lat
validates :url, presence: true
private
def set_long_lat
geocoder = Geocoder.new
data = geocoder.encode(self.address)
self.update_column(:long, data[0])
self.update_column(:lat, data[1])
end
end
My tests now:
RSpec.describe GeneralSetting, type: :model do
let (:regular) { FactoryGirl.build(:general_setting) }
describe "checking other validations" do
it "is invalid with no url" do
expect{
invalid.save
}.not_to change(GeneralSetting, :count)
end
it 'autofills the longitude' do
expect{ regular.save }.to change{ regular.long }.from(nil).to(37.568021)
end
it 'autofills the latitude' do
expect{ regular.save }.to change{ regular.lat }.from(nil).to(55.805078)
end
end
end
I would test expectation that controller calls method specified in after_action and make a separate test for that method.
Something like:
context 'POST methods' do
before(:each) do
allow(subject).to receive(:method_from_callback)
end
describe 'post create' do
before(:each) do
post :create, params: { general_setting: attributes_for(:general_setting) }
end
it 'calls :method_from_callback' do
expect(subject).to have_received(:method_from_callback)
end
end
end
describe '#method_from_callback' do
# spec for method
end
Be sure to use your method name instead of :method_from_callback pay attention that I've used rspec 3.5 syntax (wrapped request request parameters into params).

rspec expects all :resources to equal resource

I'm new to RSpec, I wonder why I didn't pass this test
before(:each) { get :index }
it "assigns all favorites as #favorites" do
favorite = FactoryGirl.create(:favorite)
expect(assigns(:favorites)).to eq([favorite])
end
It says
1) FavoritesController GET index assigns all favorites as #favorites
Failure/Error: expect(assigns(:favorites)).to eq([favorite])
expected: [#<Favorite id: 1, patient_id: 6, doctor_id: 5>]
got: #<ActiveRecord::Relation []>
(compared using ==)
Diff:
## -1,2 +1,2
-[#<Favorite:0x000000058a5ca0 id: 1, patient_id: 6, doctor_id: 5>]
+[]
It seems assigns(:favorites) got empty. I have tried another approach as well
def valid_attributes
doctor = FactoryGirl.create(:doctor)
patient = FactoryGirl.create(:patient)
FactoryGirl.attributes_for(:favorite, doctor_id: doctor.id, patient_id: patient.id)
end
it "assigns all favorites as #favorites" do
favorite = Favorite.create! valid_attributes
expect(assigns(:favorites)).to eq([favorite])
end
And it got the same error. Any inputs would be helpful for me and I'd like to ask if there is any way to simplify it.
Update
app/controllers/favorite_controller.rb
class FavoritesController < ApplicationController
before_action :set_favorite, only: [:destroy]
before_action :authenticate_user!
def index
#favorites = Favorite.where(:patient_id => current_user.id).order(id: :asc)
end
end
spec/controllers/favorite_controller_spec.rb
require 'spec_helper'
describe FavoritesController, type: :controller do
login_patient
describe "GET index" do
let!(:favorite) { FactoryGirl.create(:favorite) }
before { get :index }
it { expect(response).to render_template(:index) }
it { expect(response).to be_success }
it { expect(response).to have_http_status(200) }
it "blocks unauthenticated access", :skip_before do
expect(response).to redirect_to(new_user_session_path)
end
it "assigns all favorites as #favorites" do
expect(assigns(:favorites).to_a).to eq([favorite])
end
end
end
spec/support/controller_helpers.rb
module ControllerHelpers
def login_patient
before :each do |example|
unless example.metadata[:skip_before]
#request.env["devise.mapping"] = Devise.mappings[:user]
#patient = FactoryGirl.create(:patient)
sign_in :user, #patient
end
end
end
end
You are creating the records after you send the request so by the time the request finishes, the record you just created isn't included in the list of favorites. Change your test code to the following
let!(:favorite) { FactoryGirl.create(:favorite) }
before { get :index }
it "assigns all favorites as #favorites" do
expect(assigns(:favorites)).to eq([favorite])
end
This will probably still fail because assigns(:favorites) is an ActiveRecord::Relation object so you have to call to_a
expect(assigns(:favorites).to_a).to eq([favorite])
UPDATE:
Since the favorite is being filtered by patient, you have to make sure that the created favorite in the test belongs to the patient. You can do that by changing the favorite to
let!(:favorite) { FactoryGirl.create(:favorite, patient: #patient)
Try to create the records before the request:
let!(:favorite) { FactoryGirl.create(:favorite) }
before(:each) { get :index }
it "assigns all favorites as #favorites" do
expect(assigns(:favorites).to_a).to eq([favorite])
end

rspec test a private method within a private method that needs to be stubbed

Simplecov detected that I was missing some tests on my lib/api_verson.rb class:
class ApiVersion
def initialize(version)
#version = version
end
def matches?(request)
versioned_accept_header?(request) || version_one?(request)
end
private
def versioned_accept_header?(request)
accept = request.headers['Accept']
accept && accept[/application\/vnd\.#{Rails.application.secrets.my_app_accept_header}-v#{#version}\+json/]
end
def unversioned_accept_header?(request)
accept = request.headers['Accept']
accept.blank? || accept[/application\/vnd\.#{Rails.application.secrets.my_app_accept_header}/].nil?
end
def version_one?(request)
#version == Rails.application.secrets.my_app_default_api_version && unversioned_accept_header?(request)
end
end
This class is used by the routes file to help setup api versions:
namespace :api, path: "", defaults: {format: :json} do
scope module: :v1, constraints: ApiVersion.new(1) do
get '/alive', to: 'api#alive'
end
scope module: :v2, constraints: ApiVersion.new(2) do
get '/alive', to: 'api#alive'
end
end
This setup was ported from versioning_your_ap_is.
I am trying to test the methods here that simplecov is reporting as failures, and right now I am stuck on the case where the private method contains a private method...
def version_one?(request)
#version == Rails.application.secrets.my_app_default_api_version && unversioned_accept_header?(request)
end
This is my current spec:
require 'spec_helper'
describe ApiVersion do
before(:each) do
#apiversion = ApiVersion.new(1)
#current_api_version = Rails.application.secrets.my_app_default_api_version
#request = ActionController::TestRequest.new(host: 'localhost')
#request.headers["Accept"] = "application/vnd.#{Rails.application.secrets.my_app_accept_header}-v#{#current_api_version}+json"
end
describe "Method #versioned_accept_header? =>" do
it "Should return false if the header accept variable contains application/vnd.#{Rails.application.secrets.my_app_accept_header}-v#{#current_api_version}+json" do
expect(#apiversion.send(:unversioned_accept_header?, #request)).to eq(false)
end
it "Should return true if no version is included with the header" do
request = #request
request.headers["Accept"] = nil
expect(#apiversion.send(:unversioned_accept_header?, request)).to eq(true)
end
end
describe "Method #version_one? =>" do
it "test me" do
# #apiversion.send(:unversioned_accept_header?, #request)
binding.pry
# expect(#apiversion.send(:version_one?, #request)).to eq(false)
end
end
end
How do I stub the nested private method to test the version_one private method?
Here is how I ended up with my final tests and coverage at 100%.
The lib/api_version.rb file:
class ApiVersion
def initialize(version)
#version = version
end
def matches?(request)
versioned_accept_header?(request) || version_default?(request)
end
private
def versioned_accept_header?(request)
accept = request.headers['Accept']
accept && accept[/application\/vnd\.#{Rails.application.secrets.my_app_accept_header}-v#{#version}\+json/]
end
def unversioned_accept_header?(request)
accept = request.headers['Accept']
accept.blank? || accept[/application\/vnd\.#{Rails.application.secrets.my_app_accept_header}/].nil?
end
def version_default?(request)
#version == Rails.application.secrets.my_app_default_api_version && unversioned_accept_header?(request)
end
end
Followed up by all of the rspec tests:
require 'spec_helper'
describe ApiVersion do
before(:each) do
#apiversion = ApiVersion.new(1)
#current_api_version = Rails.application.secrets.my_app_default_api_version
#request = ActionController::TestRequest.new(host: 'localhost')
end
describe "Method #matches? =>" do
it "Should return false when the header is not versioned and it's not the header default." do
#apiversion.stub(:versioned_accept_header?).and_return(false)
#apiversion.stub(:version_default?).and_return(false)
expect(#apiversion.matches?(#request)).to eq(false)
end
it "Should return true when the proper header has been supplied but is unversioned." do
#apiversion.stub(:versioned_accept_header?).and_return(true)
#apiversion.stub(:version_default?).and_return(false)
expect(#apiversion.matches?(#request)).to eq(true)
end
it "Should return true when the proper header has been supplied and is versioned." do
#apiversion.stub(:versioned_accept_header?).and_return(true)
#apiversion.stub(:version_default?).and_return(true)
expect(#apiversion.matches?(#request)).to eq(true)
end
end
describe "Private method #unversioned_accept_header? =>" do
it "Should return false if the header accept variable contains version 'application/vnd.#{Rails.application.secrets.my_app_accept_header}' in it." do
#request.headers["Accept"] = "application/vnd.#{Rails.application.secrets.my_app_accept_header}"
expect(#apiversion.send(:unversioned_accept_header?, #request)).to eq(false)
end
it "Should return true if no version is included with the header." do
#request.headers["Accept"] = nil
expect(#apiversion.send(:unversioned_accept_header?, #request)).to eq(true)
end
end
describe "Private method #versioned_accept_header? =>" do
it "Should return true if the header accept variable contains version 'application/vnd.#{Rails.application.secrets.my_app_accept_header}.v#{Rails.application.secrets.my_app_default_api_version}+json' in it." do
#header = "application/vnd.#{Rails.application.secrets.my_app_accept_header}-v#{Rails.application.secrets.my_app_default_api_version}+json"
#request.headers["Accept"] = #header
expect(#apiversion.send(:versioned_accept_header?, #request)).to eq(#header)
end
end
describe "Private method #version_default? =>" do
it "Should return false when the proper header version is supplied." do
#apiversion.stub(:unversioned_accept_header?).and_return(false)
expect(#apiversion.send(:version_default?, #request)).to eq(false)
end
it "Should return true when no header is supplied, or a header different than 'application/vnd.#{Rails.application.secrets.my_app_accept_header}' is supplied." do
#apiversion.stub(:unversioned_accept_header?).and_return(true)
expect(#apiversion.send(:version_default?, #request)).to eq(true)
end
end
end

Destroy method in testing controller with rspec

I have a Transaction model, in which I have the following scope :
scope :ownership, -> { where property: true }
I made some tests of the controller (thanks to M. Hartl). There they are :
require 'spec_helper'
describe TransactionsController do
let(:user) { FactoryGirl.create(:user) }
let(:product) { FactoryGirl.create(:givable_product) }
before { be_signed_in_as user }
describe "Ownerships" do
describe "creating an ownership with Ajax" do
it "should increment the Ownership count" do
expect do
xhr :post, :create, transaction: { property: true, user_id: user.id, product_id: product.id }
end.to change(Transaction.ownership, :count).by(1)
end
it "should respond with success" do
xhr :post, :create, transaction: { property: true, user_id: user.id, product_id: product.id }
expect(response).to be_success
end
end
describe "destroying an ownership with Ajax" do
let(:ownership) { user.transactions.ownership.create(product_id: product.id, user_id: user.id) }
it "should decrement the Ownership count" do
expect do
xhr :delete, :destroy, id: ownership.id
end.to change(Transaction.ownership, :count).by(-1)
end
it "should respond with success" do
xhr :delete, :destroy, id: ownership.id
expect(response).to be_success
end
end
end
end
And there is the destroy method of my Transaction controller :
def destroy
#transaction = Transaction.find(params[:id])
#property = #transaction.property
#product = #transaction.product
#transaction.destroy
respond_to do |format|
format.html { redirect_to #product }
format.js
end
end
But when I run the tests, one of them fails, and I don't understand how or why :
1) TransactionsController Ownerships destroying an ownership with Ajax should decrement the Ownership count
Failure/Error: expect do
count should have been changed by -1, but was changed by 0
# ./spec/controllers/transactions_controller_spec.rb:31:in `block (4 levels) in <top (required)>'
Can you help me about it ?
You can use 'let!'.
About 'let' and 'let!': https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let
From the RSpec documentation there's a difference between let and let! (see here);
Use let to define a memoized helper method. The value will be cached
across multiple calls in the same example but not across examples.
Note that let is lazy-evaluated: it is not evaluated until the first
time the method it defines is invoked. You can use let! to force the
method's invocation before each example.
In your destroy method use let!(:ownership) so that the ownership object is not cached after it is destroyed.

Resources