Looking through a tutorial on controller testing the author gives an example of an rspec test testing a controller action. My question is, why did they use the method attributes_for over build? There is no clear explanation why attributes_for is used besides that it returns a hash of values.
it "redirects to the home page upon save" do
post :create, contact: Factory.attributes_for(:contact)
response.should redirect_to root_url
end
The tutorial link is found here: http://everydayrails.com/2012/04/07/testing-series-rspec-controllers.html The example is found at the beginning topic section Controller testing basics
attributes_for will return a hash, whereas build will return a non persisted object.
Given the following factory:
FactoryGirl.define do
factory :user do
name 'John Doe'
end
end
Here is the result of build:
FactoryGirl.build :user
=> #<User id: nil, name: "John Doe", created_at: nil, updated_at: nil>
and the result of attributes_for
FactoryGirl.attributes_for :user
=> {:name=>"John Doe"}
I find attributes_for very helpful for my functional test, as I can do things like the following to create a user:
post :create, user: FactoryGirl.attributes_for(:user)
When using build, we would have to manually create a hash of attributes from the user instance and pass it to the post method, such as:
u = FactoryGirl.build :user
post :create, user: u.attributes # This is actually different as it includes all the attributes, in that case updated_at & created_at
I usually use build & create when I directly want objects and not an attributes hash
Let me know if you need more details
Related
I'm using RSpec and Factory Girl to test my application. What I'm trying to do is the following: I have a object which accepts nested attributes, and it is not valid that nested attribute. I want to test that the POST works:
let(:valid_attributes) { build(:user).attributes }
it "creates a new User" do
expect {
post :create, {user: valid_attributes}, valid_session
}.to change(User, :count).by(1)
end
That's the factory:
FactoryGirl.define do
factory :user do |x|
name "Homer"
after(:build) do
build :address
end
end
end
The problem is that the hash returned by build(:user).attributes does not have the address, although if I inspect the object created by build(:user), the address is correctly built.
Is there some way to easily generate a hash with the nested attributes?
Answering myself to show a technically working solution:
let(:valid_attributes) {attributes_for(:user, address_attributes: attributes_for(:address))}
This works, but I find it quite clunky. In complex cases the code would get incredibly verbose and ugly. As I would expect something nicer, I'm not voting this as a solution.
You can customize your object, while building it via parameters, so I'd solve your task this way:
let(:valid_attributes) { attributes_for(:user, address: attributes_for(:address)) }
I'm trying to test a post/create controller action in my rails 4 app, but it's failing.
Here is the scaffold generated test:
it "assigns a newly created project as #project" do
post :create, {:project => valid_attributes}, valid_session
assigns(:project).should be_a(Project)
assigns(:project).should
end
Here is the code after I refactored to use it with FactoryGirl
it "assigns a newly created project as #project" do
project = FactoryGirl.create(:post)
assigns(project).should be_a(Project)
assigns(project).should be_persisted
end
So it's failing:
Failure/Error: assigns(project).should be_a(Project)
expected nil to be a kind of Project(id: integer, title: string, description: text, created_at: datetime, updated_at: datetime)
I don't know why projectis returning nil in assigns method. I already inspected it to make sure it's returning a proper Project.
Btw, here is my project factory:
factory :project do
title "MyString"
description "MyText"
users {[FactoryGirl.create(:user)]}
end
Thanks in advance!
The assigns(project) method call returns the value of the #project instance variable in Rails. Invoking the FactoryGirl method has no effect on this variable, so it evaluates to nil.
I recently started using rspec and factory_girl and I'm working on a basic control spec for my create action in my projects controller.
So I have this as a before filter:
before :each do
#project = FactoryGirl.create(:project)
#user = FactoryGirl.create(:user, first_name: "Jim", last_name: "Smith", username: "jsmith")
session[:user_id] = #user.id # this maintains the session for the user created in the previous linew
end
My project expects to have a user associated with it.
So in the create spec, I have this:
describe 'POST #create' do
attribute_merge = FactoryGirl.attributes_for(:project).merge(FactoryGirl.attributes_for(:user))
context "with valid attributes" do
it "creates a new project" do
expect{
post :create, project: attribute_merge
}.to change(Project,:count).by(1)
end
end
end
So what I'm trying to do is pass the project attributes hash along with the user attributes hash because a project requires at least one user in order to be created. Now, the error I get is:
ActiveModel::MassAssignmentSecurity::Error:
Can't mass-assign protected attributes: first_name, last_name....
I should add that my create action works perfectly in development and I do have attr_accessible :first_name, :last_name, :username,... in my user.rb file
It's failing because your passing the actual attributes for the user to the project, instead of just a reference to the user.
Try
post :create, project: FactoryGirl.build(:project, user: user).attributes
I am trying to test a "Post create" action with Rspec. The code is as follows:
def valid_attributes
{
:zone => Flymgr::Zone.new(:countries => Flymgr::ZoneCountry.first,
:name => 'USA',
:description => 'USA Flight',
:zipcodes => ''),
:price => '100.00',
:class => 'first',
}
end
def valid_session
{}
end
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
admin = FactoryGirl.create(:admin)
sign_in admin
end
describe "POST create" do
describe "with valid params" do
it "creates a new Flymgr::Rule" do
expect {
post :create, {:Flymgr_rule => valid_attributes}
}.to change(Flymgr::Rule, :count).by(1)
end
One of the required attributes for the form is a 'zone', this is a dropdown box and the options for the dropdown are created with a different form. I do not know how to create an form entry using Rspec. As you can see, I have tried to call a method from a different controller Flymgr::Zone.new. I don't think this is working and it is breaking my test.
Can anyone advise on the best way to do this? Perhaps I should be using FactoryGirl to create a zone and rule entry?
your request parameter hash has an object as value of :zone, when you post it will just be 'to_s'-ed, which is unlikely what you want.
In general the best practice is to use factory girl to build your objects and use the attributes_for strategy to parameterize its attributes for the post request:
What is the proper way to test 'create' controller actions?
Your question is suggesting that the association is a belong_to so you just need to post an id. Be aware that at present, FactoryGirl does not create any attributes for the associations. If your factory definition for rule takes care of the zone association, you can use this workaround:
FactoryGirl.build(:flymgr_rule).attributes
to also include a zone_id but, then you need to exclude the unwanted params.
("id", "created_at", "updated_at", etc).
So you may be better off explicitly insert the params hash info for zone the way you see it in a valid post request.
Read this thread on factorygirl attributes and associations:
https://github.com/thoughtbot/factory_girl/issues/359
As the guide points out:
# Returns a hash of attributes that can be used to build a User instance
attrs = FactoryGirl.attributes_for(:user)
I'm relatively new to programming, Rails, Ruby, Rspec, and the like, so thanks for your help!
My specs were very repetitive, so I wrote some spec helper methods. I can't figure out how to properly use them in my specs. Specifically, I have a users controller with create:
def create
#user = User.new(params[:user])
if #user.save
redirect_to user_path(#user)
else
render :action => :new
end
end
A bit in the spec helper that creates a valid user:
def valid_user_eilif
#test_image = Rails.root + "spec/fixtures/images/seagull.jpg"
#file = Rack::Test::UploadedFile.new(#test_image, "image/jpeg")
user = User.create!(:username => "eilif", :email => "eilif#email.org",
:image => #file, :bio => "Lots of text that I don't want to write",
:signature_quote => "Yet more text.")
user.save!
user
end
And then in my user controller spec:
before (:each) do
post :create, :user => valid_user_eilif
end
it 'should assign user to #user' do
assigns(:user).should eq(User.last)
end
When I run the spec I get the error:
Failure/Error: assigns(:user).should eq(User.last)
expected #<User id: 1, username: "eilif", email: "eilif#email.org", bio: "Lots of text that I don't want to write", signature_quote: "I feel empty.", image_file_name: "seagull.jpg", image_content_type: "image/jpeg", image_file_size: 10475, image_updated_at: "2011-05-10 23:35:55", created_at: "2011-05-10 23:35:56", updated_at: "2011-05-10 23:35:56">
got #<User id: nil, username: nil, email: nil, bio: nil, signature_quote: nil, image_file_name: nil, image_content_type: nil, image_file_size: nil, image_updated_at: nil, created_at: nil, updated_at: nil>
So, I assume I'm incorrectly posting to create, since nothing is created? What's the proper way to do this?
Ideally controller specs shouldn't depend on the model being able to create a row in the database. With such a simple action you can mock out the dependencies:
describe UsersController do
context "on success" do
before(:each) do
#user = mock_model(User,:save=>true)
User.stub(:new) {#user}
post :create, :user => {}
end
it "redirects" do
response.should redirect_to(user_path(#user))
end
it "assigns" do
assigns[:user].should == #user
end
end
context "on failure" do
it "renders 'new'" do
#user = mock_model(User,:save=>false)
User.stub(:new) {#user}
post :create, :user => {}
response.should render_template "users/new"
end
end
end
Notice that the specs don't pass anything in params[:user]. This helps enforce the MVC separation of concerns, whereby the model is responsible for handling the attributes, ie. validating, setting up associations, etc. You can't always keep controllers this 'skinny', but it's a good idea to try.
It looks like the problem is that #user doesn't get refreshed after the save. Try assigns(:user).reload.should eql(User.last).
But there's another slight problem, and that's probably still going to fail. You shouldn't be calling post with :user => valid_user_eilif; you want the attributes from your user record, not the actual user object itself. And you're essentially creating a new user in valid_user_eilif and then making your controller create that object again -- if you have any kind of unique constraints, you're going to get a conflict.
This is a good place to use something like factory_girl and mocks. For an example, take a look at how one of my projects handles controller specs. This example uses factory_girl, Mocha and shoulda. I'll annotate it with comments below:
describe MembersController, "POST create" do
before do
# Factory Girl - builds a record but doesn't save it
#resource = Factory.build(:member)
# Mocha expectation - overrides the default "new" behavior and makes it
# return our resource from above
Member.expects(:new).with({}).returns(#resource)
# Note how we expect it to be called with an empty hash; that's from the
# `:member` parameter to `post` below.
end
context "success" do
before do
post :create, :member => {}
end
# shoulda matchers - check for a flash message and a redirect
it { should set_the_flash.to(/successfully created/) }
it { should redirect_to(member_path(#resource)) }
end
context "failure" do
before do
# Mocha - To test a failing example in the controller, we override the
# default `save` behavior and make it return false, otherwise it would
# be true
#resource.expects(:save).returns(false)
post :create, :member => {}
end
# shoulda matchers - check for no flash message and re-render the form
it { should_not set_the_flash }
it { should render_template(:new) }
end
end