So I have the following models: Regions and Locations. They look fine and I can load up the page just fine. The issue gets with factory bot.
class Region < ApplicationRecord
scope :ordered, -> { order(name: :desc) }
scope :search, ->(term) { where('name LIKE ?', "%#{term}%") }
has_many :locations, dependent: :destroy
end
class Location < ApplicationRecord
scope :ordered, -> { order(name: :desc) }
scope :search, ->(term) { where('name LIKE ?', "%#{term}%") }
belongs_to :region
end
For my factories I have the following:
FactoryBot.define do
factory :region do
name 'MyString'
end
end
FactoryBot.define do
factory :location do
name 'MyString'
hours_operation 'MyString'
abbreviation 'MyString'
region nil
end
end
Both of those were rendered easily enough and the only change I made to them was switching out double quotations with single quotations. But when I run a bin/test I keep getting the following errors: Admin::LocationsController show should render the show page Failure/Error: location = FactoryBot.create(:location) ActiveRecord::RecordInvalid:Validation failed: Region must exist
Then a coordinating example of the failure: rspec ./spec/controllers/admin/locations_controller_spec.rb:16 # Admin::LocationsController show should render the show page
So I check out spec/controllers/admin/locations_controller_spec.rb and here's the code:
require 'rails_helper'
RSpec.describe Admin::LocationsController, type: :controller do
before(:each) do
activate_session(admin: true)
end
describe 'index' do
it 'should render the index' do
get :index
expect(response).to have_http_status :success
end
end
describe 'show' do
it 'should render the show page' do
location = FactoryBot.create(:location)
get :show, params: { id: location.id }
expect(response).to have_http_status :success
end
it 'should return a 404 error' do
get :show, params: { id: 1 }
expect(response).to have_http_status :not_found
end
end
describe 'new' do
it 'should render the new page' do
get :new
expect(response).to have_http_status :success
end
end
describe 'edit' do
it 'should render the edit page' do
location = FactoryBot.create(:location)
get :edit, params: { id: location.id }
expect(response).to have_http_status :success
end
end
describe 'create' do
it 'should create the record' do
expect do
post :create, params: { location:
FactoryBot.attributes_for(:location) }
end.to change(Location, :count).by(1)
end
end
describe 'update' do
it 'should update the record' do
location = FactoryBot.create(:location)
current_value = location.name
new_value = current_value + '-Updated'
expect do
patch :update, params: { id: location.id, location: { name: new_value } }
location.reload
end.to change(location, :name).to(new_value)
end
end
describe 'destroy' do
it 'should destroy the record' do
location = FactoryBot.create(:location)
expect do
delete :destroy, params: { id: location.id }
end.to change(Location, :count).by(-1)
end
end
end
The line it's references is: it 'should render the show page'.
Honestly I don't know what's causing it to fail. I thought it might be an association issues but when I went to try and do something like associations :location it caused even more errors to populate. Am I just missing something in the spec controllers for association?
EDIT/UPDATE: Fixed five of the six errors using association :region in location. But I'm getting: Failure/Error: expect do post :create, params: { location: FactoryBot.attributes_for(:location) } end.to change(Location, :count).by(1) expected #count to have changed by 1, but was changed by 0.
You can create associations in factory-bot using after_create, try this
FactoryBot.define do
factory :location do
name 'MyString'
hours_operation 'MyString'
abbreviation 'MyString'
after(:create) do |location, evaluator|
location.region = FactoryBot.create(:region)
end
end
end
All this is doing is creating a region object and associating it to the location object.
See here for more detailed guides Factory Bot Guides
Related
In a rails 6.1.3.1 API, I would like to implement an unusual test. Basically, a user can create a team with a many-to-many relational model. A team creation automatically triggers a new entry in the table JoinedTeamUser with two references (user_id and team_id), and a boolean can_edit in order to know whether the user can update/delete or not a team details.
What I would like to test is if a user accessing the update or destroy method of the team controller has the right to do so (meaning, the right entry in a model depending of another controller).
To give you a better view of the process, here is the file seed.rb.
3.times do |i|
team = Team.create(name: Faker::Company.name, description: Faker::Company.catch_phrase)
puts "Created TEAM ##{i+1} - #{team.name}"
end
10.times do |i|
name = Faker::Name.first_name.downcase
user = User.create! username: "#{name}", email: "#{name}#email.com", password: "azerty"
puts "Created USER ##{i+1} - #{user.username}"
joined_team_users = JoinedTeamUser.create!(
user_id: user.id,
team_id: Team.all.sample.id,
can_edit: true
)
puts "USER ##{joined_team_users.user_id} joined TEAM ##{joined_team_users.team_id}"
end
Edit:
As requested, here is the team controller (the filter of authorized to update user has still not been implemented) :
class Api::V1::TeamsController < ApplicationController
before_action :set_team, only: %i[show update destroy]
before_action :check_login
def index
render json: TeamSerializer.new(Team.all).serializable_hash.to_json
end
def show
render json: TeamSerializer.new(#team).serializable_hash.to_json
end
def create
team = current_user.teams.build(team_params)
if team.save
if current_user&.is_admin === false
JoinedTeamUser.create!(
user_id: current_user.id,
team_id: team.id,
can_edit: true
)
render json: TeamSerializer.new(team).serializable_hash.to_json, status: :created
else
render json: TeamSerializer.new(team).serializable_hash.to_json, status: :created
end
else
render json: {errors: team.errors }, status: :unprocessable_entity
end
end
def update
if #team.update(team_params)
render json: TeamSerializer.new(#team).serializable_hash.to_json, status: :ok
else
render json: #team.errors, status: :unprocessable_entity
end
end
def destroy
#team.destroy
head 204
end
private
def team_params
params.require(:team).permit(:name, :description, :created_at, :updated_at)
end
def set_team
#team = Team.find(params[:id])
end
end
and the team controller tests :
require "test_helper"
class Api::V1::TeamsControllerTest < ActionDispatch::IntegrationTest
setup do
#team = teams(:one)
#user = users(:one)
end
# INDEX
test "should access team index" do
get api_v1_teams_url,
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert_response :success
end
test "should forbid team index" do
get api_v1_teams_url, as: :json
assert_response :forbidden
end
# SHOW
test "should show team" do
get api_v1_team_url(#team),
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert_response :success
end
# SHOW
test "should forbid show team" do
get api_v1_team_url(#team),
as: :json
assert_response :forbidden
end
# CREATE
test "should create team" do
assert_difference('Team.count') do
post api_v1_teams_url,
params: { team: { name: "Random name", description: "Random description" } },
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
end
assert_response :created
end
test "should not create team when not logged in" do
assert_no_difference('Team.count') do
post api_v1_teams_url,
params: { team: { name: "Random name", description: "Random description" } },
as: :json
end
assert_response :forbidden
end
test "should not create team with taken name" do
assert_no_difference('Team.count') do
post api_v1_teams_url,
params: { team: { name: #team.name, description: "Random description" } },
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
end
assert_response :unprocessable_entity
end
test "should not create team without name" do
assert_no_difference('Team.count') do
post api_v1_teams_url,
params: { team: { description: "Random description"} },
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
end
assert_response :unprocessable_entity
end
# UPDATE
test "should update team" do
patch api_v1_team_url(#team),
params: { team: { name: "New name", description: "New description" } },
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert_response :success
end
test "should not update team " do
patch api_v1_team_url(#team),
params: { team: { name: "New name", description: "New description" } },
as: :json
assert_response :forbidden
end
# DESTROY
test "should destroy team" do
assert_difference('Team.count', -1) do
delete api_v1_team_url(#team),
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
end
assert_response :no_content
end
test "should forbid destroy team" do
assert_no_difference('Team.count') do
delete api_v1_team_url(#team), as: :json
end
assert_response :forbidden
end
end
Thanks !
I would start by actually fixing the models.
class Team < ApplicationRecord
has_many :memberships
has_many :members, through: :memberships
end
class Membership < ApplicationRecord
belongs_to :team
belongs_to :member, class_name: 'User'
end
class User < ApplicationRecord
has_many :memberships,
foreign_key: :member_id
has_many :teams,
through: :memberships
end
Joins should never be a part of the name of your model. Name your models after what they represent in your domain, not as peices of plumbing.
Whatever is_admin is should be admin?.
If you want to create a team and make the user who created the team a member at the same time you want to do:
# See https://github.com/rubocop/ruby-style-guide#namespace-definition
module Api
module V1
class TeamsController < ApplicationController
def create
#team = Team.new(team_params)
#team.memberships.new(
member: current_user,
can_edit: current_user.admin?
)
if team.save
# why is your JSON rendering so clunky?
render json: TeamSerializer.new(team).serializable_hash.to_json, status: :created
else
render json: {errors: team.errors }, status: :unprocessable_entity
end
end
# ...
end
end
end
This will insert the Team and Membership in a single transaction and avoids creating more code paths in the controller.
require "test_helper"
module API
module V1
class TeamsControllerTest < ActionDispatch::IntegrationTest
setup do
#team = teams(:one)
#user = users(:one)
end
# ...
test "when creating a team the creator is added as a member" do
post api_v1_teams_url,
# add valid parameters to create a team
params: { team: { ... } },
# repeating this all over your tests is very smelly
headers: { Authorization: JsonWebToken.encode(user_id: #user.id) },
as: :json
assert #user.team_memberships.exist?(
team_id: Team.last.id
), 'expected user to be a member of team'
end
end
end
end
As for actually adding the authorization checks I would recommend that you look into Pundit instead of reinventing the wheel, this also has a large degree of overlap with Rolify which is a much better alternative then adding a growning of can_x boolean columns.
I have a test in rspec that is not passing and I do not know the reason. The test accuses the following:
1) SalesmenController POST #create redirect to new team
Failure/Error: params.require(:salesmen).permit(:name, :company_id)
ActionController::ParameterMissing:
param is missing or the value is empty: salesmen
The test is:
require 'rails_helper'
RSpec.describe SalesmenController, type: :controller do
include Devise::Test::ControllerHelpers
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:owner]
#current_owner = FactoryGirl.create(:owner)
sign_in #current_owner
#current_company = FactoryGirl.create(:company, owner: #current_owner)
end
describe "POST #create" do
before(:each) do
salesman = create(:salesman, company: #current_company)
post :create, params: {:company_id => #current_company.id, company: { name: salesman.name, company_id: #current_company.id } }
end
it "redirect to new team" do
expect(response).to have_http_status(:success)
end
it "Create team with right attributes" do
expect(Salesman.last.company).to eql(#current_company)
expect(Salesman.last.name).to eql(#salesman[:name])
end
end
end
My controller is:
def create
#salesman = Salesman.new(params_salesman)
authorize! :create, #salesman
if #salesman.save
redirect_to company_salesman_path
flash[:notice] = "Salesman saved!"
else
flash.now[:error] = "Could not create salesman!"
render :new
end
end
private
def params_salesman
params.require(:salesman).permit(:name, :company_id)
end
My routes are:
Rails.application.routes.draw do
resources :companies do
resources :salesmen
resources :goals do
resources :days
end
end
devise_for :owners, :controllers => { registrations: 'registrations' }
end
My factory is:
FactoryGirl.define do
factory :salesman do
name {FFaker::Name.name}
company
end
end
Anyone know what could be happening?
Try params.permit(:name, :company_id) instead of params.require(:salesman).permit(:name, :company_id). Reason being that in your test environment, you don't have salesman coming in your params.
Testing RESTful actions of multiple Rails controllers with RSpec can generate a lot of code repetition. The following code is my first attempt at using shared examples to DRY things up.
Here is what I don't like about the code, could not find a better way and would like your help to improve:
The shared examples require that specific variables are set within let blocks within the controller spec (high coupling). I have tried to use the model name to infer the factory name and create the test data within the share examples. It works well to create the record and records variables. However, some models require the presence of associations and FactoryGirl.attributes_for does not create associated records, so validation fails. So, valid_attributes are created differently for different models. The only (likely bad) way I could think of creating valid_attributes within shared examples is to pass a string containing the code used to create the attributes and evaluate it (eval) within the shared examples
The tests that assert redirection use eval to call Rails' route/path helpers. Different controllers in this app have different redirect behaviors. After creating or updating a record, some controllers redirect to the #show action, others to #index. The problem is that when expecting a redirect to #show, AFAIK, we have to know the record ID in order to build the expected URL. And we don't know the record ID within the controller spec. We only know it within the shared examples. So how can we pass an expected redirect URL from the controller spec to the shared example if we do not yet know what that URL is (because we don't know the record ID)?
Also, please let me know if you spot any additional issues.
The controller spec:
# spec/controllers/quotes_controller_spec.rb
require "rails_helper"
RSpec.describe QuotesController, :focus, :type => :controller do
login_admin
let(:model) { Quote }
let(:record) { FactoryGirl.create(:quote) }
let(:records) { FactoryGirl.create_pair(:quote) }
let(:valid_attributes) { FactoryGirl.attributes_for(:quote, quote: "New quote") }
let(:invalid_attributes) { valid_attributes.update(quote: nil) }
include_examples "GET #index"
include_examples "GET #show"
include_examples "GET #new"
include_examples "GET #edit"
include_examples "POST #create", "quote_path(assigns(:quote))"
include_examples "PATCH #update", "quote_url"
include_examples "DELETE #destroy", "quotes_url"
end
The shared examples:
# spec/support/shared_examples/controller_restful_actions.rb
def ivar_name(model, plural: false)
if plural
model.name.pluralize.underscore.to_sym
else
model.name.underscore.to_sym
end
end
def record_name(model)
model.name.underscore.to_sym
end
RSpec.shared_examples "GET #index" do
describe "GET #index" do
it "requires login" do
sign_out current_user
get :index
expect(response).to require_login
end
it "enforces authorization" do
get :index
expect(controller).to enforce_authorization
end
it "populates instance variable with an array of records" do
get :index
expect(assigns(ivar_name(model, plural: true))).to match_array(records)
end
end
end
RSpec.shared_examples "GET #show" do
describe "GET #show" do
it "requires login" do
sign_out current_user
get :show, id: record
expect(response).to require_login
end
it "enforces authorization" do
get :show, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested record to an instance variable" do
get :show, id: record
expect(assigns(ivar_name(model))).to eq(record)
end
end
end
RSpec.shared_examples "GET #new" do
describe "GET #new" do
it "requires login" do
sign_out current_user
get :new
expect(response).to require_login
end
it "enforces authorization" do
get :new
expect(controller).to enforce_authorization
end
it "assigns a new record to an instance variable" do
get :new
expect(assigns(ivar_name(model))).to be_a_new(model)
end
end
end
RSpec.shared_examples "GET #edit" do
describe "GET #edit" do
let(:record) { FactoryGirl.create(factory_name(model)) }
it "requires login" do
sign_out current_user
get :edit, id: record
expect(response).to require_login
end
it "enforces authorization" do
get :edit, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested record to an instance variable" do
get :edit, id: record
expect(assigns(ivar_name(model))).to eq(record)
end
end
end
RSpec.shared_examples "POST #create" do |redirect_path_helper|
describe "POST #create" do
it "requires login" do
sign_out current_user
post :create, { record_name(model) => valid_attributes }
expect(response).to require_login
end
it "enforces authorization" do
post :create, { record_name(model) => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "saves the new record in the database" do
expect{
post :create, { record_name(model) => valid_attributes }
}.to change(model, :count).by(1)
end
it "assigns a newly created but unsaved record to an instance variable" do
post :create, { record_name(model) => valid_attributes }
expect(assigns(ivar_name(model))).to be_a(model)
expect(assigns(ivar_name(model))).to be_persisted
end
it "redirects to #{redirect_path_helper}" do
post :create, { record_name(model) => valid_attributes }
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not save the new record in the database" do
expect{
post :create, { record_name(model) => invalid_attributes }
}.not_to change(model, :count)
end
it "assigns a newly created but unsaved record an instance variable" do
post :create, { record_name(model) => invalid_attributes }
expect(assigns(ivar_name(model))).to be_a_new(model)
end
it "re-renders the :new template" do
post :create, { record_name(model) => invalid_attributes }
expect(response).to render_template(:new)
end
end
end
end
RSpec.shared_examples "PATCH #update" do |redirect_path_helper|
describe "PATCH #update" do
let(:record) { FactoryGirl.create(factory_name(model)) }
it "requires login" do
sign_out current_user
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(response).to require_login
end
it "enforces authorization" do
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "updates the requested record" do
patch :update, { :id => record, record_name(model) => valid_attributes }
record.reload
expect(record).to have_attributes(valid_attributes)
end
it "assigns the requested record to an instance variable" do
put :update, { :id => record, record_name(model) => valid_attributes }
expect(assigns(ivar_name(model))).to eq(record)
end
it "redirects to #{redirect_path_helper}" do
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not update the requested record" do
expect {
patch :update, { :id => record, record_name(model) => invalid_attributes }
}.not_to change { record.reload.attributes }
end
it "assigns the record to an instance variable" do
patch :update, { :id => record, record_name(model) => invalid_attributes }
expect(assigns(ivar_name(model))).to eq(record)
end
it "re-renders the :edit template" do
patch :update, { :id => record, record_name(model) => invalid_attributes }
expect(response).to render_template(:edit)
end
end
end
end
RSpec.shared_examples "DELETE #destroy" do |redirect_path_helper|
describe "DELETE #destroy" do
it "requires login" do
sign_out current_user
delete :destroy, id: record
expect(response).to require_login
end
it "enforces authorization" do
delete :destroy, id: record
expect(controller).to enforce_authorization
end
it "deletes the record" do
# Records are lazily created. Here we must force its creation.
record
expect{
delete :destroy, id: record
}.to change(model, :count).by(-1)
end
it "redirects to #{redirect_path_helper}" do
delete :destroy, id: record
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
end
Probably not an answer but too long for a comment:
First of all you can wrap all of those in a shared_examples_for block e.g.
shared_examples_for 'a CRUD Controller' do
context "GET #index" do
it "requires login" do
sign_out current_user
get :index
expect(response).to require_login
end
####
end
context "GET #show" do
it "requires login" do
sign_out current_user
get :show, id: record
expect(response).to require_login
end
####
end
end
Secondly You can have shared examples inside shared examples to the above can be
shared_examples_for 'a CRUD Controller' do
shared_examples_for 'authenticatable' do |view:,params:{}|
it "requires login" do
sign_out current_user
get view, **params
expect(response).to require_login
end
end
context "GET #index" do
it_behaves_like 'authenticatable', view: :index
####
end
context "GET #show" do
it_behaves_like 'authenticatable', view: :show, id: record
####
end
end
Third you can assign variables inside a it_behaves_like block eg.
RSpec.describe QuotesController, :focus, :type => :controller do
login_admin
it_behaves_like 'a CRUD Controller' do
let(:model) { Quote }
let(:record) { FactoryGirl.create(:quote) }
let(:records) { FactoryGirl.create_pair(:quote) }
let(:valid_attributes) { FactoryGirl.attributes_for(:quote, quote: "New quote") }
let(:invalid_attributes) { valid_attributes.update(quote: nil) }
end
end
Fourth this too can be simplified
shared_examples_for 'a CRUD Controller' do |model:|
singular,plural = 2.times.map { |n| model.name.pluralize(n).underscore.to_sym }
let(:record) { FactoryGirl.create(singular)
let(:records) {FactoryGirl.create_pair(singular) }
let(:valid_attributes) do
# build should create the nested associations correctly as long
# as your factories are right
FactoryGirl.build(singular).attributes.delete_if do |k,_|
# this is because ActiveRecord#attributes contains columns
# you don't want to be considered updateable
["id","created_at","updated_at"].include?(k)
end
end
let(:invalid_attributes) do
# create an :invalid trait in your factory so that
# you don't have to worry about the model
FactoryGirl.build(singular, :invalid).attributes.delete_if do |k,_|
["id","created_at","updated_at"].include?(k)
end
end
####
end
RSpec.describe QuotesController, :focus, :type => :controller do
login_admin
it_behaves_like 'a CRUD Controller', model: Quote
end
Finally you are going to find that using a memoized let! will help drastically since you are creating an extraordinary amount of records in those tests as it stands now. This will degrade performance drastically and if you get to a model that has certain globally unique attributes your tests will fail everywhere.
Hopefully this helps start pointing you in the right direction
Update to control testing actions
shared_examples_for 'a CRUD Controller' do |model:|
accessible_method = ->(meth) { public_methods.include?(meth) }
context "GET #index", if: controller.method_defined?(:index) do
it_behaves_like 'authenticatable', view: :index
####
end
context "GET #show", if: controller.method_defined?(:show) do
it_behaves_like 'authenticatable', view: :show, id: record
####
end
end
Here is the improved code (based on engineersmnky's suggestions). Any suggestions for further improvements are welcome.
Controller spec:
# spec/controllers/quotes_controller_spec.rb
require "rails_helper"
RSpec.describe QuotesController, :type => :controller do
it_behaves_like "a CRUD controller",
model: Quote,
create_redirect_path_helper: "quote_path(assigns(:quote))",
update_redirect_path_helper: "quote_url",
delete_redirect_path_helper: "quotes_url"
end
Shared examples:
# spec/support/shared_examples/controller_restful_actions.rb
RSpec.shared_examples "a CRUD controller" do |model:,
create_redirect_path_helper:,
update_redirect_path_helper:,
delete_redirect_path_helper:|
def self.controller_has_action?(action)
described_class.action_methods.include?(action.to_s)
end
resource_singular = model.name.underscore.to_sym
resource_plural = model.name.pluralize.underscore.to_sym
before(:each) { login_admin }
let(:record) { FactoryGirl.create(resource_singular) }
let(:records) { FactoryGirl.create_pair(resource_singular) }
# Models that validate the presence of associated records require some
# hacking in the factory to include associations in the attributes_for output.
let(:valid_attributes) { FactoryGirl.attributes_for(resource_singular) }
# All factories must have a trait called :invalid
let(:invalid_attributes) do
FactoryGirl.attributes_for(resource_singular, :invalid)
end
describe "GET #index", if: controller_has_action?(:index) do
it "requires login" do
logout
get :index
expect(response).to require_login_web
end
it "enforces authorization" do
get :index
expect(controller).to enforce_authorization
end
it "populates ##{resource_plural} with an array of #{resource_plural}" do
# Force records to be created before the request.
records
get :index
# Required when testing the User model, or else the user created
# by the Devise login helper skews the result of this test.
expected_records = assigns(resource_plural) - [#current_user]
expect(expected_records).to match_array(records)
end
end
describe "GET #show", if: controller_has_action?(:show) do
it "requires login" do
logout
get :show, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
get :show, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested #{resource_singular} to an instance variable" do
get :show, id: record
expect(assigns(resource_singular)).to eq(record)
end
end
describe "GET #new", if: controller_has_action?(:new) do
it "requires login" do
logout
get :new
expect(response).to require_login_web
end
it "enforces authorization" do
get :new
expect(controller).to enforce_authorization
end
it "assigns a new #{resource_singular} to ##{resource_singular}" do
get :new
expect(assigns(resource_singular)).to be_a_new(model)
end
end
describe "GET #edit", if: controller_has_action?(:edit) do
it "requires login" do
logout
get :edit, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
get :edit, id: record
expect(controller).to enforce_authorization
end
it "assigns #{resource_singular} to ##{resource_singular}" do
get :edit, id: record
expect(assigns(resource_singular)).to eq(record)
end
end
describe "POST #create", if: controller_has_action?(:create) do
it "requires login" do
logout
post :create, { resource_singular => valid_attributes }
expect(response).to require_login_web
end
it "enforces authorization" do
post :create, { resource_singular => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "saves the new #{resource_singular} in the database" do
expect{
post :create, { resource_singular => valid_attributes }
}.to change(model, :count).by(1)
end
it "assigns the saved #{resource_singular} to ##{resource_singular}" do
post :create, { resource_singular => valid_attributes }
expect(assigns(resource_singular)).to be_an_instance_of(model)
expect(assigns(resource_singular)).to be_persisted
end
it "redirects to #{create_redirect_path_helper}" do
post :create, { resource_singular => valid_attributes }
expect(response).to redirect_to(eval(create_redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not save the new #{resource_singular} in the database" do
expect{
post :create, { resource_singular => invalid_attributes }
}.not_to change(model, :count)
end
it "assigns the unsaved #{resource_singular} to ##{resource_singular}" do
post :create, { resource_singular => invalid_attributes }
expect(assigns(resource_singular)).to be_a_new(model)
end
it "re-renders the :new template" do
post :create, { resource_singular => invalid_attributes }
expect(response).to render_template(:new)
end
end
end
describe "PATCH #update", if: controller_has_action?(:update) do
it "requires login" do
logout
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(response).to require_login_web
end
it "enforces authorization" do
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "updates the requested #{resource_singular}" do
patch :update, { :id => record,
resource_singular => valid_attributes }
record.reload
# Required when testing Devise's User model with reconfirmable on
record.try(:confirm)
expect(record).to have_attributes(valid_attributes)
end
it "assigns the #{resource_singular} to ##{resource_singular}" do
put :update, { :id => record,
resource_singular => valid_attributes }
expect(assigns(resource_singular)).to eq(record)
end
it "redirects to #{update_redirect_path_helper}" do
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(response).to redirect_to(eval(update_redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not update the #{resource_singular}" do
# Do not attempt to "refactor" the following to any of the following:
# not_to change { quote }
# not_to change { quote.attributes }
# not_to have_attributes(invalid_attributes)
# None of the above will work. See
# https://github.com/rspec/rspec-expectations/issues/996#issuecomment-310729685
expect {
patch :update, { :id => record,
resource_singular => invalid_attributes }
}.not_to change { record.reload.attributes }
end
it "assigns the #{resource_singular} to ##{resource_singular}" do
patch :update, { :id => record,
resource_singular => invalid_attributes }
expect(assigns(resource_singular)).to eq(record)
end
it "re-renders the :edit template" do
patch :update, { :id => record,
resource_singular => invalid_attributes }
expect(response).to render_template(:edit)
end
end
end
describe "DELETE #destroy", if: controller_has_action?(:destroy) do
it "requires login" do
logout
delete :destroy, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
delete :destroy, id: record
expect(controller).to enforce_authorization
end
it "deletes the #{resource_singular}" do
# Force record to be created before the `expect` block.
# Otherwise, it is both created and deleted INSIDE the block, causing the
# count not to change.
record
expect{
delete :destroy, id: record
}.to change(model, :count).by(-1)
end
it "redirects to #{delete_redirect_path_helper}" do
delete :destroy, id: record
expect(response).to redirect_to(eval(delete_redirect_path_helper))
end
end
end
For the let blocks, does it not work if you pass in the model as a parameter to the shared example like you do with the redirect_path_helper?
include_examples "GET #index", Quote
and then in your shared_example you can use the record_name method to create record and records from FactoryGirl and generate valid_attributes and invalid_attributes (you could create an :invalid_quote factory as well for invalid attributes, not sure if that's considered a good practice/idea with FactoryGirl though) from there.
For the second problem, you don't need to use the named route helpers, url_for(controller: :quote) and url_for(#quote) should both work.
I'm building a project to learn Rails and testing, and struggling to troubleshoot errors in an RSpec test of a controller that directs a nested resource. My code works as expected in a browser. I believe the problem relates to my test set-up and the associations of FactoryGirl objects. I need help troubleshooting and fixing the controller spec.
Here's the cardio_exercises_controller.rb
class CardioExercisesController < ApplicationController
# :get_member is defined in the private method at the bottom of this file,
# and takes the member_id provided by the routing and
#converts it to a #member object.
before_action :get_member
# GET member/1/cardio_exercises
# GET member/1/cardio_exercises.json
def index
#cardio_exercises = #member.cardio_exercises
end
# GET member/1/cardio_exercises/1
# GET member/1/cardio_exercises/1.json
def show
cardio_exercise = #member.cardio_exercises.find(params[:id])
end
# GET member/1/cardio_exercises/new
def new
#member = Member.find(params[:member_id])
#cardio_exercise = #member.cardio_exercises.build
end
# GET member/1/cardio_exercises/1/edit
def edit
#cardio_exercise = #member.cardio_exercises.find(params[:id])
end
# POST member/1/cardio_exercises
# POST member/1/cardio_exercises.json
def create
#cardio_exercise = #member.cardio_exercises.build(cardio_exercise_params)
if #cardio_exercise.save
flash[:success] = "Cardio exercise was successfully created."
redirect_to member_cardio_exercises_path(#member)
else
render 'new'
end
end
# PATCH/PUT member/1/cardio_exercises/1
# PATCH/PUT member/1/cardio_exercises/1.json
def update
#cardio_exercise = #member.cardio_exercises.find(params[:id])
if #cardio_exercise.update(cardio_exercise_params)
flash[:success] = "Cardio exercise was successfully updated."
redirect_to member_cardio_exercises_path(#member)
else
render 'edit'
end
end
# DELETE member/1/cardio_exercises/1
# DELETE member/1/cardio_exercises/1.json
def destroy
#cardio_exercise = #member.cardio_exercises.find(params[:id])
#cardio_exercise.destroy
respond_to do |format|
format.html { redirect_to (member_cardio_exercises_path(#member)), notice: 'Cardio exercise was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# The get_member action converts the member_id given by the routing
# into an #member object, for use here and in the view.
def get_member
#member = Member.find(params[:member_id])
end
def cardio_exercise_params
params.require(:cardio_exercise).permit(:title, :duration, :calories_burned, :date, :member_id)
end
end
Here's the cardio_exercises_controller_spec.rb
require 'rails_helper'
RSpec.describe CardioExercisesController, :type => :controller do
before :each do
#member = FactoryGirl.create(:member)
#cardio_exercise = FactoryGirl.create(:cardio_exercise)
#cardio_exercise_attributes = FactoryGirl.attributes_for(:cardio_exercise, :member_id => #member)
end
describe "GET index" do
it "assigns all cardio_exercises as #member.cardio_exercises" do
get :index, { :member_id => #member }
expect(assigns(:cardio_exercises)).to eq(#member.cardio_exercises)
end
end
describe "GET show" do
it "assigns the requested cardio_exercise as #member.cardio_exercise" do
get :show, { :member_id => #member, :id => #cardio_exercise }
expect(assigns(:cardio_exercise)).to eq(#member.cardio_exercise)
end
end
describe "GET new" do
it "assigns a new cardio_exercise as #member.cardio_exercise" do
get :new, { :member_id => #member }
expect(assigns(:cardio_exercise)).to be_a_new(CardioExercise)
end
end
describe "GET edit" do
it "assigns the requested cardio_exercise as #member.cardio_exercise" do
end
end
describe "POST create" do
describe "with valid params" do
it "creates a new CardioExercise" do
expect {
post :create, { :member_id => #member, :cardio_exercise => #cardio_exercise_attributes }
}.to change(CardioExercise, :count).by(1)
end
it "assigns a newly created cardio_exercise as #cardio_exercise" do
post :create, { :member_id => #member, :cardio_exercise => #cardio_exercise_attributes }
expect(assigns(:cardio_exercise)).to be_a(CardioExercise)
expect(assigns(:cardio_exercise)).to be_persisted
end
it "redirects to the created cardio_exercise" do
post :create, { :member_id => #member, :cardio_exercise => #cardio_exercise_attributes }
expect(response).to redirect_to(CardioExercise.last)
end
end
end
describe "PUT update" do
describe "with invalid params" do
xit "updates the requested cardio_exercise" do
#put :update, { id: #member.id, member_id: cardio_exercise: #cardio_exercise.id }
end
xit "assigns the requested cardio_exercise as #member.cardio_exercise" do
end
xit "redirects to the cardio_exercise" do
end
end
describe "with invalid params" do
xit "assigns the cardio_exercise as #member.cardio_exercise" do
end
xit "re-renders the 'edit' template" do
expect(response).to render_template("edit")
end
end
end
describe "DELETE destroy" do
it "destroys the requested cardio_exercise" do
expect {
delete :destroy, { :member_id => #member, :id => #cardio_exercise }
}.to change(CardioExercise, :count).by(-1)
end
it "redirects to the cardio_exercises list" do
delete :destroy, { :member_id => #member, :id => #cardio_exercise }
expect(response).to redirect_to(member_cardio_exercises_url)
end
end
end
These are the relevant factories:
FactoryGirl.define do
factory :cardio_exercise do
title "My cardio exercise"
duration 30
calories_burned 300
date "2014-11-15"
association :member
end
end
FactoryGirl.define do
factory :member do
first_name {Faker::Name.first_name}
last_name {Faker::Name.last_name}
age 21
height 75
weight 195
goal "fffff" * 5
start_date "2014-11-15"
end
end
routes.rb contains:
resources :members do
resources :cardio_exercises
end
members.rb contains:
has_many :cardio_exercises, :dependent => :destroy
cardio_exercises.rb contains:
belongs_to :member
Rspec Failures/Errors:
1) CardioExercisesController GET show assigns the requested cardio_exercise as #member.cardio_exercise
Failure/Error: get :show, { :member_id => #member, :id => #cardio_exercise }
ActiveRecord::RecordNotFound:
Couldn't find CardioExercise with 'id'=25 [WHERE "cardio_exercises"."member_id" = $1]
2) CardioExercisesController POST create with valid params redirects to the created cardio_exercise
Failure/Error: expect(response).to redirect_to(CardioExercise.last)
NoMethodError:
undefined method `cardio_exercise_url' for #<CardioExercisesController:0x00000008bba960>
3) CardioExercisesController DELETE destroy destroys the requested cardio_exercise
Failure/Error: delete :destroy, { :member_id => #member, :id => #cardio_exercise }
ActiveRecord::RecordNotFound:
Couldn't find CardioExercise with 'id'=34 [WHERE "cardio_exercises"."member_id" = $1]
4) CardioExercisesController DELETE destroy redirects to the cardio_exercises list
Failure/Error: delete :destroy, { :member_id => #member, :id => #cardio_exercise }
ActiveRecord::RecordNotFound:
Couldn't find CardioExercise with 'id'=35 [WHERE "cardio_exercises"."member_id" = $1]
I think the Record Not Found errors indicate a problem with the associations between the member and cardio exercise models. The controller isn't finding the cardio exercise by its id. What have I missed in setting things up for RSpec? What's the best way to fix the set up?
The Undefined method error appears to be caused by my calling the last method on CardioExercise. I'm posting to the create method in the example. CardioExercise is the class. Can someone explain why that call triggers the error, and how to fix it?
I appreciate any help!
You create two independent objects/records: member and cardio_exercise.
You should pass #member to factory cardio_exercise to connect them.
#member = FactoryGirl.create(:member)
#cardio_exercise = FactoryGirl.create(:cardio_exercise, member: #member)
PS When you create cardio_exercise without setup member factory creates new record in table members and assign cardio_exercise with this new record
UPDATE
about "GET index"
You create #member without any associated cardio_exercises. You added them later and object #member knows nothing about it. You should reload object to fetch data from DB
expect(assigns(:cardio_exercises)).to eq(#member.reload.cardio_exercises)
and sometimes I convert relations to array and sort result to avoid failing tests when order is different
Even when the following line works just fine on the model test:
game = FactoryGirl.create(:game)
It doesn't seem to do it on games_controller_rspec.rb.
describe "GET index" do
it "assigns all games as #games" do
game = FactoryGirl.create(:game)
get :index, {}
expect(assigns(:games)).to eq([game])
end
end
And I keep getting "expected: [...]
got: nil"
This is the factory:
FactoryGirl.define do
factory :game do |f|
f.team_a_id { 1 }
f.team_b_id { 2 }
end
end
Full games_controller.rb:
class GamesController < ApplicationController
before_action :set_game, only: [:show, :edit, :update, :destroy]
before_filter :check_admin_status, only: [:new, :edit, :create, :update, :destroy]
def index
#games = Game.all
end
def show
end
def new
#game = Game.new
end
def edit
end
def create
#game = Game.new(game_params)
respond_to do |format|
if #game.save
format.html { redirect_to #game, notice: 'Game was successfully created.' }
format.json { render action: 'show', status: :created, location: #game }
else
format.html { render action: 'new' }
format.json { render json: #game.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if #game.update(game_params)
format.html { redirect_to #game, notice: 'Game was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #game.errors, status: :unprocessable_entity }
end
end
end
def destroy
#game.destroy
respond_to do |format|
format.html { redirect_to games_url }
format.json { head :no_content }
end
end
private
def set_game
#game = Game.find(params[:id])
end
def game_params
params.require(:game).permit(:team_a_id, :team_b_id)
end
end
Full games_controller_spec.rb:
require 'spec_helper'
describe GamesController do
include Devise::TestHelpers
let(:valid_attributes) { { "team_a_id" => "1" } }
let(:valid_session) { {} }
describe "GET index" do
it "assigns all games as #games" do
game = FactoryGirl.create(:game)
get :index, {}
expect(assigns(:games)).to eq([game])
end
end
describe "GET show" do
it "assigns the requested game as #game" do
game = FactoryGirl.create(:game)
get :show, {:id => game.to_param}, valid_session
expect(assigns(:game)).to eq(game)
end
end
describe "GET new" do
it "assigns a new game as #game" do
get :new, {}, valid_session
expect(assigns(:game)).to be_a_new(Game)
end
end
describe "GET edit" do
it "assigns the requested game as #game" do
game = FactoryGirl.create(:game)
get :edit, {:id => game.to_param}, valid_session
expect(assigns(:game)).to eq(game)
end
end
describe "POST create" do
describe "with valid params" do
it "creates a new Game" do
expect {
post :create, {:game => valid_attributes}, valid_session
}.to change(Game, :count).by(1)
end
it "assigns a newly created game as #game" do
post :create, {:game => valid_attributes}, valid_session
expect(assigns(:game)).to be_a(Game)
expect(assigns(:game)).to be_persisted
end
it "redirects to the created game" do
post :create, {:game => valid_attributes}, valid_session
expect(response).to redirect_to(Game.last)
end
end
describe "with invalid params" do
it "assigns a newly created but unsaved game as #game" do
allow_any_instance_of(Game).to receive(:save).and_return(false)
post :create, {:game => { "team_a_id" => "invalid value" }}, valid_session
expect(assigns(:game)).to be_a_new(Game)
end
it "re-renders the 'new' template" do
allow_any_instance_of(Game).to receive(:save).and_return(false)
post :create, {:game => { "team_a_id" => "invalid value" }}, valid_session
expect(response).to render_template("new")
end
end
end
describe "PUT update" do
describe "with valid params" do
it "updates the requested game" do
game = Game.create! valid_attributes
expect_any_instance_of(Game).to receive(:update).with({ "team_a_id" => "1" })
put :update, {:id => game.to_param, :game => { "team_a_id" => "1" }}, valid_session
end
it "assigns the requested game as #game" do
game = Game.create! valid_attributes
put :update, {:id => game.to_param, :game => valid_attributes}, valid_session
expect(assigns(:game)).to eq(game)
end
it "redirects to the game" do
game = Game.create! valid_attributes
put :update, {:id => game.to_param, :game => valid_attributes}, valid_session
expect(response).to redirect_to(game)
end
end
describe "with invalid params" do
it "assigns the game as #game" do
game = Game.create! valid_attributes
allow_any_instance_of(Game).to receive(:save).and_return(false)
put :update, {:id => game.to_param, :game => { "team_a_id" => "invalid value" }}, valid_session
expect(assigns(:game)).to eq(game)
end
it "re-renders the 'edit' template" do
game = Game.create! valid_attributes
allow_any_instance_of(Game).to receive(:save).and_return(false)
put :update, {:id => game.to_param, :game => { "team_a_id" => "invalid value" }}, valid_session
expect(response).to render_template("edit")
end
end
end
describe "DELETE destroy" do
it "destroys the requested game" do
game = Game.create! valid_attributes
expect {
delete :destroy, {:id => game.to_param}, valid_session
}.to change(Game, :count).by(-1)
end
it "redirects to the games list" do
game = Game.create! valid_attributes
delete :destroy, {:id => game.to_param}, valid_session
expect(response).to redirect_to(games_url)
end
end
end
Found it. The problem was with user privileges. I'm using devise and only allow certain methods to regular users. Therefore the result for them would be nil, unless i take the test with an admin user.
Basically I had to set an admin user on spec to make it work.
This is how you do it:
1) Write controller_macros.rb inside spec/support
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in FactoryGirl.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryGirl.create(:user)
user.confirm!
sign_in user
end
end
end
2) Add it to spec_helper
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
config.extend ControllerMacros, :type => :controller
end
3) Set the user to admin on your controller_spec
describe GamesController do
login_admin
describe "GET index" do
game = FactoryGirl.create(:game)
get :index, {}, valid_session
expect(assigns(:games).to eq([game])
end
My User factory:
FactoryGirl.define do
factory :user do
email "nn#nnn.com"
password "12345654321"
password_confirmation { "12345654321" }
factory :admin do
after(:create) { |user| user.update_attribute :admin, true }
end
end
end
Founded here.
I'm having the same exact issue. I found a post on here (I forgot to bookmark it and am having trouble finding it) that said to replace your "get :index, {}" with "controller.index" and see if it works. It worked for me when I did this. They also said it was a routing issue. What I don't understand is why, and how to fix it so that I can use the standard code that's generated from the scaffold. My route tests pass, but yet there's a routing issue that's causing "get" not to work in these tests? I don't understand.
EDIT:
Just found the post. Check it out here. The OP never comes back to tell what the issue was our how they fixed it though. I would love to know as I'm frustrated trying to figure out how to get this working.