Testing 'create' action of a nested resource with Rspec + FactoryGirl - ruby-on-rails

I have searched a lot on the internet as well as other similar questions on Stackoverflow, however I am still not sure on how to test the create method of a nested resource in my rails application.
The resource routes
resources :projects, :except => [:index, :show] do
resources :mastertags
end
Here is the action I want to test :
def create
#mastertag = #project.mastertags.build(params[:mastertag])
respond_to do |format|
if #mastertag.save
format.html { redirect_to project_mastertags_path, notice: 'Mastertag was successfully created.' }
else
format.html { render action: "new" }
end
end
end
Here is my corresponding Rspec test:
context "with valid params" do
it "creates a new Mastertag" do
project = Project.create! valid_attributes[:project]
mastertag = Mastertag.create! valid_attributes[:mastertag]
expect {
post :create, { project_id: project.id, :mastertag => valid_attributes[:mastertag] }
}.to change(Mastertag, :count).by(1)
end
end
I have a valid_attributes function as :
def valid_attributes
{ :project => FactoryGirl.attributes_for(:project_with_researcher), :mastertag => FactoryGirl.attributes_for(:mastertag) }
end
I get the following error :
Failure/Error: post :create, { project_id: project.id, :mastertag => valid_attributes[:mastertag] }
NoMethodError:
undefined method `reflect_on_association' for "5168164534b26179f30000a1":String
I also tried a couple of variations but nothing seems to work.

The answer will change slightly on your version of FactoryGirl.
The first question is, where is #projet getting created? somewhere else I guess?
You're creating both the project and the mastertag, why are you doing this?
project = Project.create! valid_attributes[:project]
mastertag = Mastertag.create! valid_attributes[:mastertag]
This is exactly what FactoryGirl does when you call Factory(:project) and Factory(:mastertag)
The next "wat", is that you create a mastertag in your spec at all. You don't use that variable anywhere. Without fixing your problem, your spec would look a lot better like this:
it "creates a new Mastertag" do
project = Factory(:project)
expect {
post :create, { project_id: project.id, :mastertag => Factory.attributes_for(:mastertag)}
}.to change(Mastertag, :count).by(1)
end
Okay, so now that we're done cleaning up the spec, lets look at your error.
Looks like it's in this line
format.html { redirect_to project_mastertags_path, notice: 'Mastertag was successfully created.' }
This path needs a project id.
format.html { redirect_to project_mastertags_path(#project), notice: 'Mastertag was successfully created.' }

#John Hinnegan's Answer is absolutely correct. i just want to add that is important, to use on the Project the Id, and not just project:
Sometimes it could be obvious to use project: in the param, but this do not work.
Works:
expect {
post :create, { project_id: project.id, :mastertag => valid_attributes[:mastertag] }
}.to change(Mastertag, :count).by(1)
Does not work:
expect {
post :create, { project: project.id, :mastertag => valid_attributes[:mastertag] }
}.to change(Mastertag, :count).by(1)

Related

Why does the second 'delete :destroy' in this spec not run?

I have a spec that is giving unexpected results. I've not been able to track down the cause. Can anyone help point me in the right direction?
let(:object1) { create :object }
let(:object2) { create :object }
let(:user) { create :user }
describe "DELETE #destroy" do
before :each do
Rails.logger.info "Object 1 ID: #{object1.id}"
Rails.logger.info "Object 2 ID: #{object4.id}"
user.roles.push Role.where(name: 'FullAccess').first
sign_in user
delete :destroy, {:id => object1.to_param}
end
it {
expect {
delete :destroy, {:id => object2.to_param}
}.to change(Object, :count).by(-1)
}
end
Results in
Failure/Error:
expect {
delete :destroy, {:id => object2.to_param}
}.to change(Object, :count).by(-1)
expected #count to have changed by -1, but was changed by 0
But if I comment out delete in the before block, the test passes.
before :each do
sign_in user
# delete :destroy, {:id => office1.to_param}
end
Why would the second object not be deleted?
edit
The method being tested is
def ObjectController < ApplicationController
load_and_authorize_resource
def destroy
Rails.logger.info "DELETE OBJECT ID: #{#object.id}"
#object.destroy
respond_to do |format|
format.html { redirect_to objects_url, notice: t('.notice') }
format.json { head :no_content }
end
end
end
Edit 2
Added logging codes to the examples above. The log output now includes
Object 1 ID: 1
Object 2 ID: 2
DELETE OBJECT ID: 1
DELETE OBJECT ID: 1
1. Lazy Evaluation
This is because your object2 is created and immediately destroyed within the expect block. let is lazy, let! is immediately evaluated
Change your let to let!, and it should work:
let!(:object1) { create :object }
let!(:object2) { create :object }
describe "DELETE #destroy" do
before :each do
sign_in user
delete :destroy, {:id => object1.to_param}
end
it {
expect {
delete :destroy, {:id => object2.to_param}
}.to change(Object, :count).by(-1)
}
end
2. Memoized object
As for the other part of the problem, the reason you're seeing the spec fail is because the controller in the before block is the same instance as in the test. The object is being set only if it's not already set, so it won't get reset on your second call to the delete method (with different params). Something like this is happening:
#object ||= Object.find(...)
Removing the delete call to the controller action under test in the before block should fix this.

Testing Create Action in Rails Controller

Testing Rails 4 Application with RSpec 3 throws an Argument Error (2 for 0) when running a test on posts#create.
My controller:
def create
#post = Post.new(post_params)
respond_to do |format|
if #post.save
format.html { redirect_to(#post,
:notice => 'Post was successfully created.') }
format.json { render :json => #post,
:status => :created, :location => #post }
else
format.html { render :action => "new" }
format.json { render :json => #post.errors,
:status => :unprocessable_entity }
end
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
My Routes:
resources :posts
My Test:
describe 'POST posts#create' do
it 'allows admin to create a new post' do
sign_out user
sign_in admin
post :create, post: { title: 'Title', body: 'Body' }
expect(response).to have_http_status(:success)
end
end
The problem is on the post :create... line of the controller test. I'm unsure of why it is not running correctly. Solutions I have attempted:
1 post :create, post: title: 'Title', body: 'Body'
Thinking there was something wrong about my syntax. This however, throws an error.
2 post :create, { title: 'Title', body: 'Body' }
I thought that the post might be implicit since it is the name of the controller. No dice.
3 post :create, post:(post_params)
Again, I thought that because I have set up the usual permitted params, that they would be necessary. Not surprisingly, the test doesn't have post_params in its current scope.
4 post :create, post: { admin: { title: 'Title', body: 'Body' } }
I had before read that you have to pass in the user creating the post item. Incorrect.
5 post :create=> { title: 'Title', body: 'Body' }
I thought it might be the case that posting implicitly creates a post because that is the controller. However, it returns the argument error.
6 post :post=> { title: 'Title', body: 'Body' }
Finally, I thought that I have to post a new post a give the parameters without having to use the create action.
Edit - 7 post(:create, post: { title: 'Title', body: 'Body' })
After reviewing the API docs (http://api.rubyonrails.org/classes/ActionController/TestCase.html), I thought this would work but it's still the same argument error.
8
describe 'POST admin#create' do
it 'allows admin to create a new post' do
sign_in admin
expect{
post :create, post: {title: 'Title', body: 'Body' }
}.to change(Post, :count).by(1)
end
end
I attempted to use expect as a block thinking that it would pick up the arguments. Nope.
Compromise Solution
describe 'POST admin#create' do
it 'allows admin to create a new post' do
sign_in admin
expect{
Post.create({title: 'Title', body: 'Body' })
}.to change(Post, :count).by(1)
end
end
I think the name of the action being the name of the controller threw RSpec off. So, instead I just manually created a post. Probably not the recommended solution.
None of these worked. What am I missing?
Error Output
Failure/Error: post(:create, post: { title: 'Title', body: 'Body' })
ArgumentError:
wrong number of arguments (2 for 0)
# ./spec/controllers/posts_controller_spec.rb:101:in `block (3 levels) in <top (required)>'
I have also taken off all of my before_filters in order to see if that was the problem, however, it gives the same error.
Your syntax looks correct. It would look like this:
describe 'POST admin#create' do
it 'allows admin to create a new post' do
sign_out user
sign_in admin
post(:create, {post: {title: 'Title', body: 'Body'}})
expect(response).to have_http_status(:success)
end
end
...but the parens and outer curlys are optional.
The error is not coming from your test. The error occurs during the post to :create. It is coming from inside the controller or the model during the :create request.

Scaffold generated rspec controller test returns nil on Factory created model

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.

How to send params with FactoryGirl (as opposed to manually sending the params as a hash)?

I have the following rspec test that works:
it "redirects to the created api_key" do
post :create, :api_key => {:api_identifier => "asdfadsf", :verification_code =>
"12345"}
response.should redirect_to(ApiKey.last) #(or any other test function)
end
But I use Factory girl so I don't have to manually create api_keys.
How can I replicate the above functionality, but use factory girl?
Using:
it "redirects to the created api_key" do
test = FactoryGirl.build(:api_key)
post :create, :api_key => test
response.should redirect_to(ApiKey.last) #(or any other test function)
end
or:
it "redirects to the created api_key" do
post :create, FactoryGirl.build(:api_key)
response.should redirect_to(ApiKey.last) #(or any other test function)
end
Gives me null values for the :api_key value when I arrive at my controller.
For reference, here is my create action that this test is testing:
def create
#api_key = ApiKey.new(params[:api_key])
#api_key.user = current_user
pp #api_key
respond_to do |format|
if #api_key.save
format.html { redirect_to #api_key, notice: 'Api key was successfully created.' }
format.json { render json: #api_key, status: :created, location: #api_key }
else
format.html { render action: "new" }
format.json { render json: #api_key.errors, status: :unprocessable_entity }
end
end
end
try:
post :create, :api_key => FactoryGirl.attributes_for(:api_key)
Using build doesn't actually create a record. It just pretends it did. Using attributes_for will give you the attributes of an object. This is mostly used in the context you describe. Note that this too will not create an object.
What I would do is this if the response is successful/redirect:
response.should be_redirect
Or even better use expect.

RSpec Newbie: Devise/Cancan causing otherwise working controller spec to fail

I'm trying to get an RSpec controller spec to pass. It's almost identical to the scaffold-generated spec, except a user is signed into devise first. If I disable 'load_and_authorize_resource' from the controller (which checks permissions), everything works fine. But if I put the line back in, it fails with:
1) PostsController logged in administrator POST create with valid params assigns a newly created post as #post
Failure/Error: post :create, :post => {'title' => 'test title'}
<Post(id: integer, title: string, cached_slug: string, content: text, user_id: integer, created_at: datetime, updated_at: datetime) (class)> received :new with unexpected arguments
expected: ({"title"=>"test title"})
got: (no args)
# ./spec/controllers/posts_controller_spec.rb:52:in `block (5 levels) in <top (required)>'
I had assumed the spec wasn't logging in the user correctly, but a puts current_user.role.name confirms the user is logged in correctly, and has the necessary role. Performing the actual process in a browser confirms it works as desired.
Anyone have any suggestions? I'm quite stumped. Controller below:
def create
#post = Post.new(params[:post])
#post.user = current_user
respond_to do |format|
if #post.save
flash[:notice] = "Post successfully created"
format.html { redirect_to(#post)}
format.xml { render :xml => #post, :status => :created, :location => #post }
else
format.html { render :action => "new" }
format.xml { render :xml => #post.errors, :status => :unprocessable_entity }
end
end
end
...And the spec
describe "with valid params" do
it "assigns a newly created post as #post" do
Post.stub(:new).with({'title' => 'test title'}) { mock_post(:save => true) }
post :create, :post => {'title' => 'test title'}
assigns(:post).should be(mock_post)
end
...And supporting stuff in the spec:
before(:each) do
#user = Factory(:admin)
sign_in #user
end
def mock_post(stubs={})
#mock_post ||= mock_model(Post, stubs).as_null_object
end
Many thanks...
Try upgrading CanCan to version 1.5. I had the issue earlier but I think it went away when I upgraded.

Resources