Rails Functional Test Failing Due to Association - ruby-on-rails

I have an accounts model that holds some basic account info (account name, website, etc). I then have a user model that has the following in the app/models/user.rb
belongs_to :account
I also have the following in my routes.rb
map.resources :account, :has_many => [:users, :othermodel]
the problem I'm facing is that the following test is failing:
test "should create user" do
assert_difference('User.count') do
post :create, :user => { } #this is the line it's actually failing on
end
assert_redirected_to user_path(assigns(:user)) #it doesn't get here yet
end
The error it gives is "Can't find Account without ID" so I kind of understand WHY it's failing, because of the fact that it doesn't have the account object (or account_id as it were) to know under what account to create the user. I have tried variations of the following but I am completely lost:
post :create, :user => { accounts(:one) } #I have the 'fixtures :accounts' syntax at the top of the test class
post :create, [accounts(:one), :user] => { }
post :create, :user => { accounts(:one), #other params for :user }
and like I said, just about every variation I could think of. I can't find much documentation on doing this and this might be why people have moved to Factories for doing test data, but I want to understand things that come standard in Rails before moving onto other things.
Can anyone help me get this working?
UPDATE:
I managed to get the test to fail in a different location, I had to ensure that the test could actually get to the create action (have some authlogic stuff in my app)
it now says
undefined method 'users' for nil:Class
So now it's saying that it can't find a users collection on my #account object in the controller, basically because it still can't find the #account even though it doesn't actually fail to find the account in question. So my before_filter :find_account works to the extent that it doesn't break, but it seem to not be finding the account.
I tried the post :create, :id => #account.id, :user => { } but to no avail. Also tried post :create, :account => accounts(:one), :user => { } and :user => { :account => accounts(:one) }, again with the same result.

I think you got the association backwards. If that's the case then Account model should belong_to :user, then User model should has_one :account.
Otherwise, since you're creating user which belongs to some account, you should pass account's :id in params:
post :create, :id => some_test_account.id, :user => {...}

The typical controller for the belongs_to side of a has_many association would have a create action like this:
def create
#account = Account.find(params[:account_id])
#user = #account.users.build(params[:user])
if #account.save
# handle success
else
# handle failure
end
end
If your controller doesn't look like this you not be handling the parameters correctly, thus the test failure.
You can also check your routes. You should see something like:
POST /accounts/:account_id/users(.:format) {:controller=>"users", :action=>"create"}
That's another clue that Rails is setting params[:account_id] to the value for the requested account.

Related

How do I make my redirect include the name instead of an ID?

I'm using Rails 5.1. In my controller, I would like to redirect to my "show" method like so
redirect_to(#organization)
but I would like the URL to appear as
/organization/organization_name
instead of
/organization/primary_key_id
How do I set this up? I already have a field "name" in my Organization model.
Edit: As requested, this is the index method of my PagesController ...
class PagesController < ApplicationController
# Should be the home page
def index
worker_id = params[:worker_id]
worker = Worker.find_by_id(worker_id)
if worker && worker.organization
redirect_to(worker.organization)
else
render :file => "#{Rails.root}/public/404", layout: false, status: 404
end
end
end
Edit: My config/routes.rb file
resources :organizations, :only => [:show] do
post :update_work
get :get_work
get :mine
get :poll
post :submit
get :home
get :terms_of_use
end
Here's the app/model/stratum_worker.rb file
class StratumWorker < ApplicationRecord
has_one :organization_worker
has_one :organization, :through => :organization_worker
OK, if you are not interested to use any gem then you can without gem like
class Model < ApplicationRecord
def to_param # overridden
organization_name
end
end
in this case, you need to make sure the organization_name name is unique, for uniqueness the organization_name you can use validation like this
validates_uniqueness_of :organization_name
then the model will look like this
class Model < ApplicationRecord
validates_uniqueness_of :organization_name
def to_param # overridden
organization_name
end
end
and now to the controller using find_by_organization_name(params[:organization_name]) instead of find(params[:id]).
Second Option
You can not change anything to your controller if used like this in just model
class Model < ApplicationRecord
def to_param # overridden
organization_name
"#{id} #{organization_name}".parameterize
end
end
then the URL looks like this /10-microsoft.
See this to_param method. The complete reference of with gem or without gem Rails Friendly URLs
RailsCasts.com created an episode for Pretty URLs with FriendlyId, can you check it out for getting the idea.
From Comment
I don't think what's going on but sure something wrong with the relationship, can you check like this
redirect_to(worker.organizations.first)
#=> OR
redirect_to organization_path(worker.organizations.first.id)
Update
I think worker.organization are missing somehow, would you try like this?
if worker && worker.organizations.present?
redirect_to(worker.organizations.first)
....
the present method making sure worker.organizations not blank.
I don't know about the relationship, you can try like this and let me know what's happening if it's not working then I strongly recommend to post the models with relationship concept.
Update 2 after question update
At first, you don't need the through relationship because it uses too Many To Many relationships. Your relationship is One To One then your model will look like this
class StratumWorker < ApplicationRecord
has_one :organization_worker
....
has_one :organization, :through => :organization_worker
organization_worker.rb file like this
class OrganizationWorker < ApplicationRecord
belongs_to :stratum_worker
#=> Add code what you need like for URL which was the actual motive in this post
....
Then the action looks like this
def index
worker_id = params[:worker_id]
worker = StratumWorker.find_by_id(worker_id)
if worker && worker.organization_worker.present?
#redirect_to(worker.organization_worker)
redirect_to organization_path(worker.organization_worker)
else
render :file => "#{Rails.root}/public/404", layout: false, status: 404
end
end
and the show action
OrganizationWorker.find(params:id)
I think the problem will solve now. If still, you getting errors then please read the One To One relationship again & again until clearing the relationship concept.
Hope it will help.
I wrote a post here detailing exactly this a while ago. Most of my answer will be from there. The relevant Rails documentation for this is here.
Quick definitions:
Slug: part of the URL to identify the record, in your case organization_name
Primary key: a unique identifier for database records. This usually is and should be id.
Summary
If you type organization_path(#organization), it'll automatically use the id attribute in the URL. To adjust to using organization_name, you'll need to make 2 changes:
Override the route params in your routes.rb file.
Override the to_param method in the model
1. Override The Route Params
At the moment, if you run rails routes your routes look like so:
organizations GET /organizations(.:format) organizations#index
POST /organizations(.:format) organizations#create
new_organization GET /organizations/new(.:format) organizations#new
edit_organization GET /organizations/:id/edit(.:format) organizations#edit
organization GET /organizations/:id(.:format) organizations#show
PATCH /organizations/:id(.:format) organizations#update
PUT /organizations/:id(.:format) organizations#update
DELETE /organizations/:id(.:format) organizations#destroy
The edit_organization and organization paths use id as a parameter to lookup your organization.
Use this to override the route params
Rails.application.routes.draw do
resources :organizations, param: :organization_name
end
Now rails routes will show that your routes look like so:
organizations GET /organizations(.:format) organizations#index
POST /organizations(.:format) organizations#create
new_organization GET /organizations/new(.:format) organizations#new
edit_organization GET /organizations/:organization_name/edit(.:format) organizations#edit
organization GET /organizations/:organization_name(.:format) organizations#show
PATCH /organizations/:organization_name(.:format) organizations#update
PUT /organizations/:organization_name(.:format) organizations#update
DELETE /organizations/:organization_name(.:format) organizations#destroy
2. Override The Model Params
By default organization.to_param will return the id of the organization. This needs to be overridden, do this by modifying your Model:
class Organization < ApplicationRecord
def to_param
organization_name
end
end
Conclusion & Warning
You can now continue using your redirects and forms as usual, but instead of the route using the id, it'll now use the organization name.
Also, good luck with your mining pool! Lemme know which coin you're mining and I might join!
Also, I didn't cover this because it isn't a part of your original question, but, you should ensure that the organization_name is unique! Not only should you add a uniqueness constraint validates :organization_name, uniqueness: true in the mode, you should also enforce it at the database level in your migration.
Addendum 1: Customizing for routs
When your routes are defined as so:
resources :organizations, :only => [:show] do
post 'update_work'
get 'get_work'
get 'mine'
get 'poll'
post 'submit'
get 'home'
get 'terms_of_use'
end
Your routes will be as so:
organization_update_work POST /organizations/:organization_id/update_work(.:format) organizations#update_work
organization_get_work GET /organizations/:organization_id/get_work(.:format) organizations#get_work
organization_mine GET /organizations/:organization_id/mine(.:format) organizations#mine
organization_poll GET /organizations/:organization_id/poll(.:format) organizations#poll
organization_submit POST /organizations/:organization_id/submit(.:format) organizations#submit
organization_home GET /organizations/:organization_id/home(.:format) organizations#home
organization_terms_of_use GET /organizations/:organization_id/terms_of_use(.:format) organizations#terms_of_use
organization GET /organizations/:id(.:format) organizations#show
Changing the param like so:
resources :organizations, :only => [:show], param: :organization_name do
post 'update_work'
get 'get_work'
get 'mine'
get 'poll'
post 'submit'
get 'home'
get 'terms_of_use'
end
Will change your routes to
organization_update_work POST /organizations/:organization_organization_name/update_work(.:format) organizations#update_work
organization_get_work GET /organizations/:organization_organization_name/get_work(.:format) organizations#get_work
organization_mine GET /organizations/:organization_organization_name/mine(.:format) organizations#mine
organization_poll GET /organizations/:organization_organization_name/poll(.:format) organizations#poll
organization_submit POST /organizations/:organization_organization_name/submit(.:format) organizations#submit
organization_home GET /organizations/:organization_organization_name/home(.:format) organizations#home
organization_terms_of_use GET /organizations/:organization_organization_name/terms_of_use(.:format) organizations#terms_of_use
organization GET /organizations/:organization_name(.:format) organizations#show
Which should work totally fine with your redirect.
Method that is called under the hood for id generation is to_param
so in your case to get your desired result you should add this to your Organization class:
class Organization < ApplicationRecord
...
def to_param
name
end
...
end
!!!WARNING!!! - since Rails is also using the parameter on the other side (e.g. in show method Organization.find(params[:id]) uses the URL id), now it will be params[:id] == "some_organization_name" so change your instance lookups accordingly - in show action for example use Organization.find_by!(name: params[:id]) and so on
As for your routing error - make sure that worker.organization is not nil.
There is a gem friendly_id that does exactly what you are asking for: https://github.com/norman/friendly_id
You add,
gem 'friendly_id'
Then bundle install and run rails generate friendly_id and rails db:migrate
to your Gemfile and,
class Organization < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
end
to your model then,
class OrganizationController < ApplicationController
def show
#user = Organization.friendly.find(params[:id])
end
end
to your controller.
This prevents the issues you can run into in Kkulikovskis answer where you have to make sure that you are looking things up correctly.

Testing POST #create in RSpec with nested attributes - can't get params hash correct

Have been spending some months trying to grok RSpec/TDD. Running into some challenges testing a controller with a nested attribute. I'm a bit fuzzy about how to properly set up and pass the parameters to the controller. I can build the controller so that it actually works as expected in the browser - I just can't seem to reverse engineer a test to confirm that.
Would appreciate any recommendations to a) fix the test below, and b) advise any better ways to do it (e.g. with mocks, stubs, etc).
Here's the basic model structure:
class School < ActiveRecord::Base
has_many :scholarships
end
class Scholarship < ActiveRecord::Base
belongs_to :school
end
I've configured the routes.rb as you'd expect, with:
resources :schools do
resources :scholarships, only: [:new, :create, :destroy]
end
In the controller, #new and #create are pretty standard for a Rails app:
class ScholarshipsController < ApplicationController
def new
#school = School.find(params[:school_id])
#scholarship = #school.scholarships.build
end
def create
#school = School.find(params[:school_id])
#scholarship = #school.scholarships.create(scholarship_params)
if #scholarship.save
flash[:success] = 'Scholarship created!'
redirect_to #school
else
render 'new'
end
end
private
def scholarship_params
params.require(:scholarship).permit(:name, ** Lots of params omitted for brevity **,
:notes, school: [:id])
end
end
The spec is where I can't seem to figure things out. For spec/controllers/scholarships_controller_spec.rb:
require 'rails_helper'
describe ScholarshipsController, type: :controller do
describe 'POST #create' do
context 'with valid attributes' do
before :each do
#school = create(:school)
#scholarship = #school.scholarships.create(FactoryGirl.build(:scholarship).attributes)
end
it 'receives :save' do
post :create, { scholarship: #scholarship.attributes, school: #school.id }
expect(#scholarship).to receive(:save)
end
end
end
end
When I run that test, I get the following error:
Failures:
1) ScholarshipsController POST #create with valid attributes receives :save
Failure/Error: post :create, scholarship: { attributes: #scholarship.attributes, school: #school.id } #school_id: #school.id, scholarship: #scholarship
ActionController::UrlGenerationError:
No route matches {:action=>"create", :controller=>"scholarships",
:scholarship=>{"id"=>"1", "name"=>"Dynamic Metrics Director Scholarship", *** Lots of parameters omitted for brevity ***
, "school_id"=>"2"}, :school=>"2"}
The parameters look correct to me. there's a set of attributes for the scholarship, and for the school. But the routing isn't working. I've tried a dozen different ways to try and get this to work. Heartened that I'm apparently passing a (more or less correct) parameters hash, but can't figure out quite where I'm going wrong.
****** EDIT ******
Updated in response to an answer posted below.
Changed the syntax of the spec as suggested by Srdjan:
it 'receives :save' do
post :create, "schools/#{#school.id}/scholarships", { scholarship: #scholarship.attributes, school_id: #school.id }
expect(#scholarship).to receive(:save)
end
This changes the error message. I assume that indicates that the parameters are being passed correctly, since it's no longer throwing an error related to routes/params..? Error message is:
1) ScholarshipsController POST #create with valid attributes receives :save
Failure/Error: expect(#scholarship).to receive(:save)
(#<Scholarship:0x007fe293b02598>).save(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Just for good measure, here are the relevant routes, which I hadn't posted previously:
school_scholarships POST /schools/:school_id/scholarships(.:format) scholarships#create
new_school_scholarship GET /schools/:school_id/scholarships/new(.:format) scholarships#new
school_scholarship DELETE /schools/:school_id/scholarships/:id(.:format) scholarships#destroy
In your test, you're POST-ing to the wrong route. As setup in routes.rb, scholarship resources do not exist out of the context of a school resource.
In order to fix this, you have to answer a question: "Does it make sense for a user to access a scholarship record without having to specify a school?"
If the answer is yes, you can either copy the scholarships route and paste them outside of the schools resource block. This way, you can have access to scholarships without having to specify a school, but also with specifying a school.
If the answer to the question is no, then you need to fix your test as such:
it 'receives :save' do
post :create, "schools/#{#school.id}/scholarhips", { scholarship: #scholarship.attributes, school_id: #school.id }
expect(#scholarship).to receive(:save)
end

Create rest complex resource instance in a single rest call

I would like to create complex rest object instances with a single rest call using rails.
In the example case below I get an error in the controller when I call new on Person with a parameter hash.
I get an error for unexpected type when seeing a ActiveSupport::HashWithIndifferentAccess and not a PhoneNumber
The hash passed from the test contains an array of Hash objects, while the controller action parameters create ActiveSupport::HashWithIndifferentAccess objects.
Any suggestions to fix the error?
Is there an easier way to create complex activerecord objects with a single rest call.
ie models:
class Person < ActiveRecord::Base
has_many :phone_numbers , :autosave => true
class PhoneNumber < ActiveRecord::Base
belongs_to :person
person_controller_test.rb
test "should create person" do
newperson=Person.new(:name => "test")
newperson.phone_numbers << PhoneNumber.new(:number => "123-4567")
person_string= newperson.to_xml(:include => :phone_numbers)
person_hash=Hash.from_xml(course_string)
person_hash2=person_hash['person']
post :create, :person => person_hash2, :format => "xml"
assert_response :success
end
person_controller.rb
def create
#person = Person.new(params[:person])
class Person < ActiveRecord::Base
has_many :phone_numbers , :autosave => true
# this is important for create complex nested object in one call
accepts_nested_attributes_for :phone_numbers
end
class PhoneNumber < ActiveRecord::Base
belongs_to :person
end
person_controller_test.rb
test "should create person" do
newperson=Person.new(:name => "test")
newperson.phone_numbers.build(:number => "123-4567") #more cleaner
# and start from here I'm not sure but this maybe help you
# I think that you must pass a json object
post :create, :person => newperson.to_json(:include => :phone_numbers), :format => "xml"
assert_response :success
end
link: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Dinatih, Thanks for the helpful answer! It helped solve the issue.
I ran into a slight problem since with "accepts_nested_attributes_for :phone_numbers",
the hash key 'phone_numbers_attributes' is needed instead of the to_xml/to_json serialization default of 'phone_numbers'. The test code (below) looks a little ugly, but it passes and creates the object correctly. Also passing json to the post method unfortunately doesn't create the object.
test "should create complex person" do
newperson=Person.new(:name => "test")
newperson.phone_numbers.build(:number => "123-4567")
person_string= newperson.to_xml(:include => :phone_numbers)
person_hash=Hash.from_xml(person_string)
person_hash2=person_hash['person']
person_hash2[:phone_numbers_attributes] = person_hash2['phone_numbers']
person_hash2.delete('phone_numbers')
p person_hash2
post :create, :person => person_hash2, :format => "xml"
p response.body
assert_select "person" do
assert_select "name", {:text=>"test"}
assert_select "phone-numbers" do
assert_select "phone-number" do
assert_select "number", {:text=>"123-4567"}
end
end
end
assert_response :success
end
you should also check out:
Gem nested_form :
https://github.com/ryanb/nested_form
examples for nested_form: https://github.com/ryanb/complex-form-examples/tree/nested_form
and
RailsCasts 196 / 197
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2

RSpec: Expectation on model's not working while testing controller

I'm trying to write a functional test. My test looks as following:
describe PostsController do
it "should create a Post" do
Post.should_receive(:new).once
post :create, { :post => { :caption => "ThePost", :category => "MyCategory" } }
end
end
My PostsController (a part of it actually) looks as following:
PostController < ActiveRecord::Base
def create
#post = Post.new(params[:post])
end
end
Running the test I'm always receiving a failure, which says that the Post class expected :new but never got it. Still, the actual post is created.
I'm a newbie to RSpec. Am I missing something?
EDIT - Threw away the previous rubbish
This should do what you want
require File.dirname(__FILE__) + '/../spec_helper'
describe PostsController do
it "should create a Post" do
attributes = {"Category" => "MyCategory", "caption" => "ThePost"}
Post.stub!(:new).and_return(#post = mock_model(Post, :save => false))
Post.should_receive(:new).with( attributes ).and_return #post
post :create, { :post => attributes }
end
end
This assumes you are using rspecs own mocking library and that you have the rspec_rails gem installed.
You can use the controller method of Rspec-rails to test message expectations on controllers, as described here. So one way of testing your create action is like so:
describe PostsController do
it "should create a Post" do
controller.should_receive(:create).once
post :create, { :post => { :caption => "ThePost", :category => "MyCategory" } }
end
end
EDIT (making an argument)
You might want to consider whether it's a good idea to write a test that depends on the implementation of the create action. If you're testing for anything other than the proper responsibilities of a controller, you run the risk of breaking tests when refactoring, and having to go back and rewrites tests when the implementation changes.
The job of the create action is to create something -- so test for that:
Post.count.should == 1
and then you know whether a Post was created, without depending on how it was created.
EDIT #2 (um...)
I see from your original question that you already know the Post is being created. I'd still argue that you should test for behavior, not implementation, and that checking for whether the model receives a message is not a good thing in a controller test. Maybe what you're doing is debugging, not testing?

Nested Resource testing RSpec

I have two models:
class Solution < ActiveRecord::Base
belongs_to :owner, :class_name => "User", :foreign_key => :user_id
end
class User < ActiveRecord::Base
has_many :solutions
end
with the following routing:
map.resources :users, :has_many => :solutions
and here is the SolutionsController:
class SolutionsController < ApplicationController
before_filter :load_user
def index
#solutions = #user.solutions
end
private
def load_user
#user = User.find(params[:user_id]) unless params[:user_id].nil?
end
end
Can anybody help me with writing a test for the index action? So far I have tried the following but it doesn't work:
describe SolutionsController do
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.build(:solution, :owner => #user)}
#user.stub!(:solutions).and_return(#solutions)
end
it "should find all of the solutions owned by a user" do
#user.should_receive(:solutions)
get :index, :user_id => #user.id
end
end
And I get the following error:
Spec::Mocks::MockExpectationError in 'SolutionsController GET index, when the user owns the software he is viewing should find all of the solutions owned by a user'
#<User:0x000000041c53e0> expected :solutions with (any args) once, but received it 0 times
Thanks in advance for all the help.
Joe
EDIT:
Thanks for the answer, I accepted it since it got my so much farther, except I am getting another error, and I can't quite figure out what its trying to tell me:
Once I create the solutions instead of build them, and I add the stub of the User.find, I see the following error:
NoMethodError in 'SolutionsController GET index, when the user owns the software he is viewing should find all of the solutions owned by a user'
undefined method `find' for #<Class:0x000000027e3668>
It's because you build solution, not create. So there are not in your database.
Made
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.create(:solution, :owner => #user)}
#user.stub!(:solutions).and_return(#solutions)
end
And you mock an instance of user but there are another instance of User can be instanciate. You need add mock User.find too
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.create(:solution, :owner => #user)}
User.stub!(:find).with(#user.id).and_return(#user)
#user.stub!(:solutions).and_return(#solutions)
end
I figured out my edit, when a find is done from the params, they are strings as opposed to actual objects or integers, so instead of:
User.stub!(:find).with(#user.id).and_return(#user)
I needed
User.stub!(:find).with(#user.id.to_s).and_return(#user)
but thank you so much shingara you got me in the right direction!
Joe

Resources