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.
Related
I'm trying to test the 'destroy' action for my nested comments controller.
User model has_many :comments, dependent: :destroy
Movie model has_many :comments, dependent: :destroy
Comments model belongs_to :user and :movie
Here is my comments controller
def create
#comment = #movie.comments.new(comment_params.merge(user: current_user))
if #comment.save
flash[:notice] = 'Comment successfully added'
redirect_to #movie
else
flash.now[:alert] = 'You can only have one comment per movie'
render 'movies/show'
end
end
def destroy
#comment = #movie.comments.find(params[:id])
if #comment.destroy
flash[:notice] = 'Comment successfully deleted'
else
flash[:alert] = 'You are not the author of this comment'
end
redirect_to #movie
end
private
def comment_params
params.require(:comment).permit(:body)
end
def set_movie
#movie = Movie.find(params[:movie_id])
end
Of course there is also before_action :set_movie, only: %i[create destroy] at the top.
Here are my specs, I'm using FactoryBot and all factories works fine in other examples so I think the issue is somewhere else.
describe "DELETE #destroy" do
let(:user) { FactoryBot.create(:user) }
let(:movie) { FactoryBot.create(:movie) }
before do
sign_in(user)
end
it "deletes comment" do
FactoryBot.create(:comment, movie: movie, user: user)
expect do
delete :destroy, params { movie_id: movie.id }
end.to change(Comment, :count).by(-1)
expect(response).to be_successful
expect(response).to have_http_status(:redirect)
end
end
I've got an error ActionController::UrlGenerationError: No route matches {:action=>"destroy", :controller=>"comments", :movie_id=>1}
I think my address in specs destroy action is wrong but how to define it in a good way?
You need to specify id of a comment you want to remove:
it "deletes comment" do
comment = FactoryBot.create(:comment, movie: movie, user: user)
expect do
delete :destroy, params { id: comment.id, movie_id: movie.id }
end.to change(Comment, :count).by(-1)
# ...
end
I want to contribute here with one more approach. Sometimes you have to be sure that you've deleted the exact instance (comment_1, not comment_2).
it "deletes comment" do
comment_1 = FactoryBot.create(:comment, movie: movie, user: user)
comment_2 = FactoryBot.create(:comment, movie: movie, user: user)
delete :destroy, params { id: comment_1.id, movie_id: movie.id }
expect { comment_1.reload }.to raise_error(ActiveRecord::RecordNotFound)
# ...
end
Somehow I was never able to pass an ID, either in params {} or as a direct argument following "delete". Instead, I made it work like so:
it "DELETE will cause an exception" do
delete "/comments/#{comment.id}"
expect { comment.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
I have a problem with redirecting and testing it in rspec
I have a problem with test not passing when I use a get method, but are green for the same code base when I use a put method. I don't know how to work around this and need assistance in making tests pass.
I get a 200 HTTP status code, but I want to get confirmation on redirect, so that it could be tracked by rspec. What code basically needs to do is redirect logged in user who is not owner of a product to category_product_url(category, product) with a flash error: 'You are not allowed to edit this product.' when trying to edit product with get http method.
Used gems rspec-rails, devise and decent_exposure. Ruby 2.1.5 and Rails 4.1.8
Actual error message:
Failure/Error: expect(response).to redirect_to(category_product_url(category, product))
Expected response to be a <redirect>, but was <200>
Failure/Error: expect(controller.flash[:error]).to eq 'You are not allowed to edit this product.'
expected: "You are not allowed to edit this product."
got: nil
(compared using ==)
My spec
context 'another user is singed in' do
let(:user) { create(:user) }
let(:user2) { build(:user) }
let(:product) { Product.create! valid_attributes }
before do
sign_in user2
controller.stub(:user_signed_in?).and_return(true)
controller.stub(:current_user).and_return(user2)
controller.stub(:authenticate_user!).and_return(user2)
product.user = user
end
describe 'GET edit' do
describe 'with valid params' do
it 'redirects to product page' do
get :edit, { id: product.to_param, category_id: category.to_param }
expect(response).to redirect_to(category_product_url(category, product))
end
it 'renders error message' do
get :edit, { id: product.to_param, category_id: category.to_param }
expect(controller.flash[:error]).to eq 'You are not allowed to edit this product.'
end
end
end
My controller
before_action :authenticate_user!, only: [:new, :edit, :update, :destroy, :create]
expose(:category)
expose(:products)
expose(:product)
def edit
end
def update
if product.user == current_user
if self.product.update(product_params)
redirect_to category_product_url(category, product), notice: 'Product was successfully updated.'
else
render action: 'edit'
end
else
redirect_to category_product_url(category, product), flash: { error: 'You are not allowed to edit this product.' }
end
end
private
def product_params
params.require(:product).permit(:title, :description, :price, :category_id, :user_id)
end
What is strange, is that put method is working fine with the same update action. Following specs are passing
describe 'PUT update' do
describe 'with valid params' do
it 'redirects to product page' do
put :update, { id: product.to_param, product: { 'title' => 'MyString' }, category_id: category.to_param }
expect(response).to redirect_to(category_product_url(category, product))
end
it 'does not update product' do
put :update, { id: product.to_param, product: { 'title' => 'MyNewString' }, category_id: category.to_param }
expect(controller.product.title).to_not eq 'MyNewString'
end
it 'renders error message' do
put :update, { id: product.to_param, product: { 'title' => 'MyString' }, category_id: category.to_param }
expect(controller.flash[:error]).to eq 'You are not allowed to edit this product.'
end
end
end
The way how decent_exposure and devise work, you need to first invoke a before_action method passing there a private method of your choice.
For this particular example your controller should contain this
before_action :author!, only: [:edit, :update]
and a private method filter
def author!
unless self.product.user == current_user
redirect_to category_product_url(category, product),
flash: { error: 'You are not allowed to edit this product.' }
end
end
This way you get your get http requests and put http requests pass the specs.
Try this
expect(:get => "/products/1/edit").to route_to(:controller => "controller_name", :action => "action_name")
Hope this will work.
I have a problem with workshop app that i'm doing right now. I can't fix two last errors from tests. In my opinion application is working fine in browser. Maybe there is something wrong with tests? Any help would be appreciated.
Two errors:
1) ProductsController PUT update with valid params updates the requested product
Failure/Error: Unable to find matching line from backtrace
Exactly one instance should have received the following message(s) but didn't: update
2) ProductsController PUT update with invalid params re-renders the 'edit' template
Failure/Error: response.should render_template("edit")
expecting <"edit"> but rendering with <[]>
Test code:
require 'spec_helper'
describe ProductsController do
let(:category) { create(:category) }
let(:valid_attributes) { { "title" => "MyString", "category_id" => category.id, "price" => 5.59,
"description" => "Lorem ipsum dolor sit amet"} }
let(:valid_session) { {} }
describe "PUT update" do
let(:user) { build(:user) }
before do
sign_in user
controller.stub(:user_signed_in?).and_return(true)
controller.stub(:current_user).and_return(user)
controller.stub(:authenticate_user!).and_return(user)
end
describe "with valid params" do
it "updates the requested product" do
product = Product.create! valid_attributes
Product.any_instance.should_receive(:update).with({ "title" => "MyString" })
put :update, { id: product.to_param, product: { "title" => "MyString" }, category_id:
category.to_param }, valid_session
end
describe "with invalid params" do
it "re-renders the 'edit' template" do
product = Product.create! valid_attributes
Product.any_instance.stub(:save).and_return(false)
put :update, { id: product.to_param, product: { "title" => "invalid value" }, category_id:
category.to_param }, valid_session
response.should render_template("edit")
end
end
end
end
ProductsController#update code:
def update
if self.product.update(product_params)
redirect_to category_product_url(category, product), notice: 'Product was successfully
updated.'
else
render action: 'edit'
end
end
In general
expecting <"edit"> but rendering with <[]>
Typically this means that you are expecting a render (following a validation failure for example) and your controller perform a redirect (after a successful saving of your model)
In you code
You stub the save method here:
Product.any_instance.stub(:save).and_return(false)
But call an action that uses the update method
if self.product.update(product_params)
So the action succeed -> your controller redirects -> your 'edit' template is not rendered -> your spec fails
Your solution
Instead of stubbing save, you should stub valid?, it's the good practice in that case
Product.any_instance.stub(:valid?).and_return(false)
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)
I'm having trouble understanding why I can't seem to stub this controller method :load_user, since all of my tests fail if I change the actual implementation of :load_user to not return and instance of #user.
Can anybody see why my stub (controller.stub!(:load_user).and_return(#user)) seems to fail to actually get called when RSpec makes a request to the controller?
require 'spec_helper'
describe TasksController do
before(:each) do
#user = Factory(:user)
sign_in #user
#task = Factory(:task)
User.stub_chain(:where, :first).and_return(#user)
controller.stub!(:load_user).and_return(#user)
end
#GET Index
describe "GET Index" do
before(:each) do
#tasks = 7.times{Factory(:task, :user => #user)}
#user.stub!(:tasks).and_return(#tasks)
end
it "should should find all of the tasks owned by a user" do
#user.should_receive(:tasks).and_return(#tasks)
get :index, :user_id => #user.id
end
it "should assign all of the user's tasks to the view" do
get :index, :user_id => #user.id
assigns[:tasks].should be(#tasks)
end
end
#GET New
describe "GET New" do
before(:each) do
#user.stub_chain(:tasks, :new).and_return(#task)
end
it "should return a new Task" do
#user.tasks.should_receive(:new).and_return(#task)
get :new, :user_id => #user.id
end
end
#POST Create
describe "POST Create" do
before(:each) do
#user.stub_chain(:tasks, :new).and_return(#task)
end
it "should create a new task" do
#user.tasks.should_receive(:new).and_return(#task)
post :create, :user_id => #user.id, :task => #task.to_s
end
it "saves the task" do
#task.should_receive(:save)
post :create, :user_id => #user.id, :task => #task
end
context "when the task is saved successfully" do
before(:each) do
#task.stub!(:save).and_return(true)
end
it "should set the flash[:notice] message to 'Task Added Successfully'"do
post :create, :user_id => #user.id, :task => #task
flash[:notice].should == "Task Added Successfully!"
end
it "should redirect to the user's task page" do
post :create, :user_id => #user.id, :task => #task
response.should redirect_to(user_tasks_path(#user.id))
end
end
context "when the task isn't saved successfully" do
before(:each) do
#task.stub(:save).and_return(false)
end
it "should return to the 'Create New Task' page do" do
post :create, :user_id => #user.id, :task => #task
response.should render_template('new')
end
end
end
it "should attempt to authenticate and load the user who owns the tasks" do
context "when the tasks belong to the currently logged in user" do
it "should set the user instance variable to the currently logged in user" do
pending
end
end
context "when the tasks belong to another user" do
it "should set the flash[:notice] to 'Sorry but you can't view other people's tasks.'" do
pending
end
it "should redirect to the home page" do
pending
end
end
end
end
class TasksController < ApplicationController
before_filter :load_user
def index
#tasks = #user.tasks
end
def new
#task = #user.tasks.new
end
def create
#task = #user.tasks.new
if #task.save
flash[:notice] = "Task Added Successfully!"
redirect_to user_tasks_path(#user.id)
else
render :action => 'new'
end
end
private
def load_user
if current_user.id == params[:user_id].to_i
#user = User.where(:id => params[:user_id]).first
else
flash[:notice] = "Sorry but you can't view other people's tasks."
redirect_to root_path
end
end
end
Can anybody see why my stub doesn't work? Like I said, my tests only pass if I make sure that load_user works, if not, all my tests fail which makes my think that RSpec isn't using the stub I created.
Stubbing out load_user breaks your tests because stubbing the method neuters it. When the controller calls load_user, it is no longer running your original code. It's now just returning whatever you specify in and_return(...) (which is getting returned to the ActionController callback stack, which ignores anything other than false).
Your controller code isn't using the return value of that method; it's using the variable instantiated within it. Since the original code for the load_user method isn't being run, the #user instance variable is never instantiated. (The #user variable in your tests is only visible to your tests.)
But with all the other stubs you have, I don't see any reason why you should need to stub out load_user at all. As long as you're stubbing current_user to return #user (which I assume is being done in the sign_in method), then there shouldn't be any need.
you can also try to verify that the stub works by doing an assertion like
controller.current_user.should == #user