I used this tutorial to generate an API for my Rails 5 app.
This is between Comments and Post tables. Post has a 1:m relationship with Comment.
But rspec fails every test. I have checked my files and hierarchy over and over again, still not seeing where the problem is.
Here is the output of rails routes:
Prefix Verb URI Pattern Controller#Action
post_comments GET /posts/:post_id/comments(.:format) comments#index
POST /posts/:post_id/comments(.:format) comments#create
post_comment GET /posts/:post_id/comments/:id(.:format) comments#show
PATCH /posts/:post_id/comments/:id(.:format) comments#update
PUT /posts/:post_id/comments/:id(.:format) comments#update
DELETE /posts/:post_id/comments/:id(.:format) comments#destroy
post_like POST /posts/:post_id/like(.:format) posts#like
post_dislike POST /posts/:post_id/dislike(.:format) posts#dislike
posts GET /posts(.:format) posts#index
POST /posts(.:format) posts#create
post GET /posts/:id(.:format) posts#show
PATCH /posts/:id(.:format) posts#update
PUT /posts/:id(.:format) posts#update
DELETE /posts/:id(.:format) posts#destroy
my config/routes.rb:
Rails.application.routes.draw do
resources :posts do
resources :comments
post :like
post :dislike
end
end
application_controller.rb: (Even with ActionController::API, it does not work)
class ApplicationController < ActionController::Base
include Response
include ExceptionHandler
protect_from_forgery with: :exception
end
My comments_controller.rb
class CommentsController < ApplicationController
before_action :set_post
before_action :set_post_comment, only: [:show, :update, :destroy]
# GET /comments
# GET /comments.json
def index
##comments = Comment.all
json_response(#post.comments)
end
# GET /comments/1
# GET /comments/1.json
def show
json_response(#comment)
end
# POST /comments
# POST /comments.json
def create
#comment = Comment.new(comment_params)
#post.comments.create!(comment_params)
json_response(#post, :created)
if #comment.save
render :show, status: :created, location: #comment
else
render json: #comment.errors, status: :unprocessable_entity
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_comment
#comment = Comment.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def comment_params
params.require(:comment).permit(:commenter, :comment, :description, :post)
end
def set_post
#post = Post.find(params[:post_id])
end
def set_post_comment
#comment = #post.comments.find_by!(id: params[:id]) if #post
end
end
Finally, the posts_controller.rb:
class PostsController < ApplicationController
before_action :set_post, only: [:show, :update, :destroy]
# GET /posts
# GET /posts.json
def index
#posts = Post.all
json_response(#posts)
end
# GET /posts/1
# GET /posts/1.json
def show
json_response(#post)
end
# POST /posts
# POST /posts.json
def create
#post = Post.new(post_params)
json_response(#post, :created)
if #post.save
render :show, status: :created, location: #post
else
render json: #post.errors, status: :unprocessable_entity
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_post
#post = Post.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def post_params
params.require(:post).permit(:poster, :vote, :description, :comment, :user_id, :image_base)
end
end
UPDATE: showing spec files
my spec/requests/comments_spec.rb:
require 'rails_helper'
RSpec.describe "Comments", type: :request do
# Initialize the test data
let!(:post) { create(:post) }
let!(:comments) { create_list(:comment, 20, post_id: post.id) }
let(:post_id) { post.id }
let(:id) { comments.first.id }
# Test suite for GET /posts/:post_id/comments
describe 'GET /posts/:post_id/comments' do
before { get "/posts/#{post_id}/comments" }
context 'when post exists' do
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
it 'returns all post comments' do
expect(json.size).to eq(20)
end
end
context 'when post does not exist' do
let(:post_id) { 0 }
it 'returns status code 404' do
expect(response).to have_http_status(404)
end
it 'returns a not found message' do
expect(response.body).to match(/Couldn't find Post/)
end
end
end
# Test suite for GET /posts/:post_id/comments/:id
describe 'GET /posts/:post_id/comments/:id' do
before { get "/posts/#{post_id}/comments/#{id}" }
context 'when post item exists' do
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
it 'returns the item' do
expect(json['id']).to eq(id)
end
end
context 'when post item does not exist' do
let(:id) { 0 }
it 'returns status code 404' do
expect(response).to have_http_status(404)
end
it 'returns a not found message' do
expect(response.body).to match(/Couldn't find Comment/)
end
end
end
# Test suite for PUT /posts/:post_id/comments
describe 'POST /posts/:post_id/comments' do
let(:valid_attributes) { { comment: 'A Comment', commenter: 'Luke Shaw' } }
context 'when request attributes are valid' do
before { post "/posts/#{post_id}/comments", params: valid_attributes }
it 'returns status code 201' do
expect(response).to have_http_status(201)
end
end
context 'when an invalid request' do
before { post "/posts/#{post_id}/comments", params: {} }
it 'returns status code 422' do
expect(response).to have_http_status(422)
end
it 'returns a failure message' do
expect(response.body).to match(/Validation failed: Commenter or Comment can't be blank/)
end
end
end
end
The spec/requests/posts_spec.rb:
require 'rails_helper'
RSpec.describe "Posts", type: :request do
# initialize test data
let!(:posts) { create_list(:post, 10) }
let(:post_id) { posts.first.id }
describe "GET /posts" do
# make HTTP get request before each example
before { get '/posts' }
it 'returns posts' do
# Note `json` is a custom helper to parse JSON responses
expect(json).not_to be_empty
expect(json.size).to eq(10)
end
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
end
# Test suite for GET /posts/:id
describe 'GET /posts/:id' do
before { get "/posts/#{post_id}" }
context 'when the record exists' do
it 'returns the post' do
expect(json).not_to be_empty
expect(json['id']).to eq(post_id)
end
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
end
context 'when the record does not exist' do
let(:post_id) { 100 }
it 'returns status code 404' do
expect(response).to have_http_status(404)
end
it 'returns a not found message' do
expect(response.body).to match(/Couldn't find Post/)
end
end
end
The spec/models/post_spec.rb:
require 'rails_helper'
RSpec.describe Post, type: :model do
# Association test
# ensure Post model has a 1:m relationship with the Comment model
it { should have_many(:comments).dependent(:destroy) }
# Validation tests
# ensure columns are present before saving
it { should validate_presence_of(:poster) }
it { should validate_presence_of(:description) }
end
ANd spec/models/comment_spec.rb:
require 'rails_helper'
RSpec.describe Comment, type: :model do
# Association test
# ensure a comment record belongs to a single post record
it { should belong_to(:post) }
# Validation test
# ensure column name is present before saving
it { should validate_presence_of(:comment) }
it { should validate_presence_of(:commenter) }
end
UPDATE 2: Showing spec/factories/*.rb files
Showing spec/factories/posts.rb:
FactoryBot.define do
factory :post do
image { Rack::Test::UploadedFile.new(Rails.root.join('public', 'system', 'posts', 'images', '000', '000', '001', 'original', 'img1.jpeg'), 'image/jpeg') }
poster { Faker::Lorem.sentence }
vote { Faker::Number.number(10).to_i}
description { Faker::Lorem.paragraph }
end
end
Showing spec/factories/comments.rb:
FactoryBot.define do
factory :comment do
commenter { Faker::Lorem.sentence }
description { Faker::Lorem.paragraph }
post_id nil
end
end
My `routes show clear inheritance and I am enforcing that in my controllers. Still, I get errors like:
Failure/Error: expect(:delete => "/comments/1").to route_to("comments#destroy", :id => "1")
No route matches "/comments/1"
AND
RuntimeError:
/home/user/projects/app/public/system/posts/images/000/000/001/original/img1.jpeg file does not exist
Ok, so the error that is causing every spec to fail is that uploaded image. It can't find the image file that you are trying to upload. Where is that image file actually located? Create that file and put it in the right place and they should start working again.
As to the routing-spec - that's the error that you included in your question, and it'll be localised to your routing-spec only (spec/routing/comments_routing_spec.rb according to your paste-bin), and will be fixed by just adjusting the routes to be the nested ones. They should include everything necessary to make the routes work (including a valid post-id).
Secondly, it should be something like:
expect(:delete => "/posts/1/comments/1").to route_to("comments#destroy", :id => "1", :post_id => "1")
Note: personally I don't bother with route-specs for simple resources, just for complex ones, but you may prefer it as a backup while you're getting used to Rails.
Related
I'm following along with a tutorial on testing with Rspec. Getting a syntax error when trying to run the test. Here's my test code:
require 'rails_helper'
RSpec.describe CommentsController, type: :controller do
describe "comments#create action" do
it "should allow admins to create comments on posts" do
post = FactoryGirl.create(:post)
admin = FactoryGirl.create(:admin)
sign_in admin
post :create, params: { post_id: post.id, comment: { message: 'awesome post' } }
expect(response).to redirect_to root_path
expect(post.comments.length).to eq 1
expect(post.comments.first.message).to eq "awesome gram"
end
it "should require an admin to be logged in to comment on a post" do
post = FactoryGirl.create(:post)
post :create, params: { post_id: post.id, comment: { message: 'awesome post' } }
expect(response).to redirect_to new_admin_session_path
end
it "should return http status code of not found if the post isn't found" do
admin = FactoryGirl.create(:admin)
sign_in admin
post :create, params: { post_id: 'SUNSHINE', comment: { message: 'awesome post'} }
expect(response).to have_http_status :not_found
end
end
end
Here's the controller:
class CommentsController < ApplicationController
before_action :authenticate_admin!, only: [:create]
def create
#post = Post.find_by_id(params[:post_id])
return render_not_found if #post.blank?
#post.comments.create(comment_params.merge(admin: current_admin))
redirect_to root_path
end
private
def render_not_found(status=:not_found)
render plain: "#{status.to_s.titleize} :(", status: status
end
def comment_params
params.require(:comment).permit(:message)
end
end
Here's the terminal output when running the test:
Terminal output
What's odd is that when I comment the lines that are producing the error, the tests run as intended with the last test passing. I've checked the test file along with similar posts that describe the same problem and it looks to me like the syntax is correct. New to Rails so pardon the rookie mistakes.
The problem is that you have a variable named post:
post = FactoryGirl.create(:post) # define `post` variable
post :create, params: ... # try to call `post` method
Therefore, on subsequent lines post will refer to the variable, not the post method.
Solution 1: Rename the variable
my_post = FactoryGirl.create(:post)
post :create, params: { post_id: my_post.id, ... }
Solution 2: Use self.post to access the method
post = FactoryGirl.create(:post)
self.post :create, params: { post_id: post.id, ... }
Replicating the issue in irb:
def foo
"bar"
end
foo = "wat"
#=> "wat"
foo :hello
# SyntaxError: (irb):63: syntax error, unexpected ':', expecting end-of-input
# foo :hello
# ^
I think it has to do with you defining a variable named post then trying to call the Rspec method post:
it "should require an admin to be logged in to comment on a post" do
post = FactoryGirl.create(:post)
post :create, params: { post_id: post.id, comment: { message: 'awesome post' } }
expect(response).to redirect_to new_admin_session_path
end
Try re-naming it:
to call the Rspec method post:
it "should require an admin to be logged in to comment on a post" do
message = FactoryGirl.create(:post)
post :create, params: { post_id: message.id, comment: { message: 'awesome post' } }
expect(response).to redirect_to new_admin_session_path
end
Background
I have created a movie review app that allows a logged in user to add and edit a movie as well as leave a review for each movie.
Problem
I am working on testing my controllers, however I keep getting:
3) Error:
ReviewsControllerTest#test_should_get_edit:
ActionController::UrlGenerationError: No route matches
{:action=>"edit_movie_review", :controller=>"reviews"}
test/controllers/reviews_controller_test.rb:34:in `block in <class:ReviewsControllerTest>'
4) Error:
ReviewsControllerTest#test_should_get_new:
ActionController::UrlGenerationError: No route matches {:action=>"new", :controller=>"reviews"}
test/controllers/reviews_controller_test.rb:17:in `block in <class:ReviewsControllerTest>'
5) Error:
ReviewsControllerTest#test_should_show_review:
ActionController::UrlGenerationError: No route matches {:action=>"show", :controller=>"reviews", :id=>"1"}
test/controllers/reviews_controller_test.rb:29:in `block in <class:ReviewsControllerTest>'
6) Error:
ReviewsControllerTest#test_should_create_review:
ActionController::UrlGenerationError: No route matches {:action=>"create", :controller=>"reviews", :review=>{:comment=>"MyText", :rating=>"1"}}
test/controllers/reviews_controller_test.rb:23:in `block (2 levels) in <class:ReviewsControllerTest>'
test/controllers/reviews_controller_test.rb:22:in `block in <class:ReviewsControllerTest>'
Rake routes:
POST /movies/:movie_id/reviews(.:format) reviews#create
new_movie_review GET /movies/:movie_id/reviews/new(.:format) reviews#new
edit_movie_review GET /movies/:movie_id/reviews/:id/edit(.:format) reviews#edit
movie_review GET /movies/:movie_id/reviews/:id(.:format) reviews#show
PATCH /movies/:movie_id/reviews/:id(.:format) reviews#update
PUT /movies/:movie_id/reviews/:id(.:format) reviews#update
DELETE /movies/:movie_id/reviews/:id(.:format) reviews#destroy
movies GET /movies(.:format) movies#index
POST /movies(.:format) movies#create
new_movie GET /movies/new(.:format) movies#new
edit_movie GET /movies/:id/edit(.:format) movies#edit
movie GET /movies/:id(.:format) movies#show
PATCH /movies/:id(.:format) movies#update
PUT /movies/:id(.:format) movies#update
ReviewsControllerTest:
require 'test_helper'
class ReviewsControllerTest < ActionController::TestCase
setup do
#review = reviews(:one)
#user = users(:one)
end
test "should get index" do
get :index
assert_response :success
assert_not_nil assigns(:review)
end
test "should get new" do
get :new
assert_response :success
end
test "should create review" do
assert_difference('Review.count') do
post :create, review: { comment: #review.comment, rating: #review.rating }
end
assert_redirected_to review_path(assigns(:review))
end
test "should show review" do
get :show, id: #review
assert_response :success
end
test "should get edit" do
get :edit_movie_review, #review
assert_response :success
end
test "should update review" do
put :update
assert_redirected_to review_path(assigns(:review))
end
test "should destroy review" do
assert_difference('Review.count', -1) do
delete :destroy, id: #review
end
assert_redirected_to reviews_path
end
end
ReviewsController.rb:
class ReviewsController < ApplicationController
before_action :find_movie
before_action :find_review, only: [:edit, :update, :destroy]
before_action :authenticate_user!, only: [:new, :edit]
def new
#review = Review.new
end
def create
#review = Review.new(review_params)
#review.user_id = current_user.id
#review.movie_id = #movie.id
if #review.save #if a review is succesfully saved, redirect user to home
redirect_to movie_path(#movie)
else
render 'new'
end
end
def edit
end
def update
if #review.update(review_params)
redirect_to movie_path(#movie)
else
render 'edit'
end
end
def destroy
#review.destroy
redirect_to movie_path(#movie)
end
private
def review_params
params.require(:review).permit(:rating, :comment)
end
def find_movie
#movie = Movie.find(params[:movie_id])
end
def find_review
#review = Review.find(params[:id])
end
end
I am new to the world of programming therefore any form of advice is much appreciated.
Thanks in advance!
You can pass movie_id with your routes. Try this for your new action test and you can follow the same pattern for other actions as well.
Note: You ReviewsController does not have index and show action.
require 'test_helper'
class ReviewsControllerTest < ActionController::TestCase
setup do
#movie = movies(:one)
#review = reviews(:one)
#user = users(:one)
end
#test "should get index" do
# get :index
# assert_response :success
# assert_not_nil assigns(:review)
#end
test "should get new" do
get :new, movie_id: #movie.id
assert_response :success
end
test "should create review" do
assert_difference('Review.count') do
post :create, movie_id: #movie.id ,review: { comment: #review.comment, rating: #review.rating }
end
assert_redirected_to movie_path(assigns(:review))
end
test "should get edit" do
get :edit, movie_id: #movie.id, id: #review.id
assert_response :success
end
test "should update review" do
put :update, movie_id: #movie.id, id: #review.id, review: { comment: #review.comment, rating: #review.rating }
assert_redirected_to movie_path(assigns(:review))
end
test "should destroy review" do
assert_difference('Review.count', -1) do
delete :destroy, id: #review, movie_id: #movie.id
end
end
end
I am building a simple blog app in order to learn BDD/TDD with RSpec and Factory Girl. Through this process, I continue to run into 'Failures' but I believe they have more to do with how I am using Factory Girl than anything.
As you'll see below, in order to get my specs to pass, I'm having a hard time keeping my test DRY - there must be something I am misunderstanding. You'll notice, I'm not using Factory Girl to it's full potential and at times, skipping it altogether. I find that I commonly run into problems when using functions such as get :create, get :show, or put :update within the spec.
I am currently stuck on the #PUT update spec that should simply test the assignment of the #post variable. I have tried multiple types of this spec that I found online, yet none seem to work - hence, is it Factory Girl? Maybe the specs I'm finding online are outdated Rspec versions?
I'm using:
Rspec 3.1.7
Rails 4.1.6
posts_controller_spec.rb
require 'rails_helper'
require 'shoulda-matchers'
RSpec.describe PostsController, :type => :controller do
describe "#GET index" do
it 'renders the index template' do
get :index
expect(response).to be_success
end
it "assigns all posts as #posts" do
post = Post.create(title: 'Charlie boy', body: 'Bow wow wow ruff')
get :index
expect(assigns(:posts)).to eq([post])
end
end
describe '#GET show' do
it 'assigns the request post to #post' do
post = Post.create!(title: 'Charlie boy', body: 'Bow wow wow ruff')
get :show, id: post.id
expect(assigns(:post)).to eq(post)
end
end
describe '#GET create' do
context 'with valid attributes' do
before :each do
post :create, post: attributes_for(:post)
end
it 'creates the post' do
expect(Post.count).to eq(1)
expect(flash[:notice]).to eq('Your post has been saved!')
end
it 'assigns a newly created post as #post' do
expect(assigns(:post)).to be_a(Post)
expect(assigns(:post)).to be_persisted
end
it 'redirects to the "show" action for the new post' do
expect(response).to redirect_to Post.first
end
end
context 'with invalid attributes' do
before :each do
post :create, post: attributes_for(:post, title: 'ha')
end
it 'fails to create a post' do
expect(Post.count).to_not eq(1)
expect(flash[:notice]).to eq('There was an error saving your post.')
end
it 'redirects to the "new" action' do
expect(response).to redirect_to new_post_path
end
end
end
describe '#GET edit' do
it 'assigns the request post to #post' do
post = Post.create!(title: 'Charlie boy', body: 'Bow wow wow ruff')
get :edit, id: post.id
expect(assigns(:post)).to eq(post)
end
end
describe '#PUT update' do
context 'with success' do
before :each do
post :create, post: attributes_for(:post)
end
it 'assigns the post to #post' do
put :update, id: post.id
expect(assigns(:post)).to eq(post)
end
end
end
end
posts_controller.rb
class PostsController < ApplicationController
def index
#posts = Post.all.order('created_at DESC')
end
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
if #post.save
flash[:notice] = "Your post has been saved!"
redirect_to #post
else
flash[:notice] = "There was an error saving your post."
redirect_to new_post_path
end
end
def show
#post = Post.find(params[:id])
end
def edit
#post = Post.find(params[:id])
end
def update
#post = Post.find(params[:id])
# if #post.update(params[:post].permit(:title, :body))
# flash[:notice] = "Your post is updated!"
# redirect_to #post
# else
# flash[:notice] = "There was an error updating your post."
# render :edit
# end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
end
factories/post.rb
FactoryGirl.define do
factory :post do
title 'First title ever'
body 'Forage paleo aesthetic food truck. Bespoke gastropub pork belly, tattooed readymade chambray keffiyeh Truffaut ennui trust fund you probably haven\'t heard of them tousled.'
end
end
Current Failure:
Failures:
1) PostsController#PUT update with success assigns the post to #post
Failure/Error: put :update, id: post.id
ArgumentError:
wrong number of arguments (0 for 1+)
# ./spec/controllers/posts_controller_spec.rb:86:in `block (4 levels) in <top (required)>'
Finished in 0.19137 seconds (files took 1.17 seconds to load)
17 examples, 1 failure
You could definitely leverage factories here.
The factory you've created is actually fine too.
Instead of doing:
post = Post.create(title: 'Charlie boy', body: 'Bow wow wow ruff')
Do this: post = FactoryGirl.create(:post)
You can get ever more DRY if you do this:
# in spec/rails_helper.rb
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end
This will allow you do this in your spec: post = create(:post)
Regarding your PUT test, try this from a previous SO answer:
describe '#PUT update' do
let(:attr) do
{ :title => 'new title', :content => 'new content' }
end
context 'with success' do
before :each do
#post = FactoryGirl.create(:post)
end
it 'assigns the post to #post' do
put :update, :id => #post.id, :post => attr
#post.reload
expect(assigns(:post)).to eq(post)
end
end
end
Edit:
Also, don't be afraid of moving things in to a before :each do if you need to. They are great at keeping things DRY
The immediate reason why your spec is failing is because you can only call on the controller once per test, and for update you're calling it twice: in the before-action, you are calling create... and then in the main part of the update test you are calling update... controller specs don't like that.
In order to get the existing spec working, you would need to replace the post :create, post: attributes_for(:post) line in the before-action with just creating a post or (as mentioned already) using factory girl to create a post - rather than trying to do it by calling the controller to do it.
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
I'm making an app where people can create jobs, but can only destroy the jobs they've created. I know that my app lets people destroy jobs because I can test it manually, but RSpec is lagging behind.
Here's the relevant test:
jobs_controller_spec.rb
require 'spec_helper'
describe JobsController do
let!(:user) { FactoryGirl.create(:user) }
let!(:job) { FactoryGirl.create(:job, user: user) }
let!(:wrong_user) { FactoryGirl.create(:user, email: "wrong#example.com") }
let!(:wrong_job) { FactoryGirl.create(:job, user: wrong_user) }
[...]
describe "correct user control" do
before { sign_in user }
describe "users can only delete their own jobs" do
it "should not change job count" do
expect do
delete :destroy, id: wrong_job.id
end.to_not change(Job, :count)
end
end
describe "users can delete their own jobs" do
it "should decrease job count" do
expect do
delete :destroy, id: job.id
end.to change(Job, :count).by(-1)
end
end
end
end
Here's the failing test:
1) JobsController correct user control users can delete their own jobs should decrease job count
Failure/Error: expect do
count should have been changed by -1, but was changed by 0
# ./spec/controllers/jobs_controller_spec.rb:41:in `block (4 levels) in <top (required)>'
jobs_controller.rb
class JobsController < ApplicationController
skip_before_action :require_signin, only: [:index, :show]
skip_before_action :correct_user, only: [:index, :show, :new, :create]
before_action :set_job, only: [:show, :edit, :update, :destroy]
[...]
def destroy
#job.destroy
respond_to do |format|
format.html { redirect_to jobs_url }
format.json { head :no_content }
end
end
private
def set_job
#job = Job.find(params[:id])
end
def job_params
params.require(:job).permit(:title, :org, :internship, :postdate, :filldate, :location, :link, :description)
end
end
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_filter :require_signin
before_filter :correct_user
include SessionsHelper
private
def require_signin
unless signed_in?
store_location
redirect_to signin_url, notice: "Please sign in."
end
end
def correct_user
#job = current_user.jobs.find_by(id: params[:id])
redirect_to root_url if #job.nil?
end
end
rake routes
Prefix Verb URI Pattern Controller#Action
jobs GET /jobs(.:format) jobs#index
POST /jobs(.:format) jobs#create
new_job GET /jobs/new(.:format) jobs#new
edit_job GET /jobs/:id/edit(.:format) jobs#edit
job GET /jobs/:id(.:format) jobs#show
PATCH /jobs/:id(.:format) jobs#update
PUT /jobs/:id(.:format) jobs#update
DELETE /jobs/:id(.:format) jobs#destroy
[...]
As Peter Alfvin points out, if there is authentication in controller specs you have to setup a session and pass it as third parameter, for example:
...
let(:some_user) { User.create }
def valid_session
{ user_id: some_user.id }
end
describe "DELETE destroy" do
it "destroys the requested job" do
job = Job.create! valid_attributes
expect {
delete :destroy, { :id => job.to_param }, valid_session
}.to change(Job, :count).by(-1)
end
end
The solution is to pass no_capybara: true to sign_in. Capybara doesn't work with controller tests, so one can't use capybara to manage the sign in process.
Thanks to Patrick Brinich-Langlois for the solution.