I'm playing around with RSpec to make sure caching is properly working.
I have a simple controller
module Api::V1
class ArticlesController < Api::V1::BaseController
def index
#articles = Rails.cache.fetch(Article.cache_key) do
p "IT IS A MISS" # Added for debugging
Article.all
end
end
end
end
and the spec below
RSpec.describe Api::V1::ArticlesController, type: :request do
...
it 'cache?', :caching do
create_list(:article, 2)
get '/api/v1/articles.json'
expect(JSON.parse(response.body).size).to eq(2)
create(:article)
get '/api/v1/articles.json'
expect(JSON.parse(response.body).size).to eq(2)
end
end
with the following configuration
RSpec.configure do |config|
config.around(:each, :caching) do |example|
Rails.cache.clear
caching = ActionController::Base.perform_caching
ActionController::Base.perform_caching = example.metadata[:caching]
example.run
ActionController::Base.perform_caching = caching
end
end
To make the spec pass and insure the cache is a hit I have defined cache_key as follows
def self.cache_key
'articles'
end
However, even if the cache is a miss only for the first request, the spec fails.
% RUBYOPT="-W0" bin/rspec spec/requests/api/v1/articles_controller_spec.rb:35
"IT IS A MISS"
F
Failures:
1) Api::V1::ArticlesController cache? is expected to eq 2
Failure/Error: expect(JSON.parse(response.body).size).to eq(2)
expected: 2
got: 3
(compared using ==)
# ./spec/requests/api/v1/articles_controller_spec.rb:39:in `block (4 levels) in <top (required)>'
# ./spec/support/config/capybara.rb:21:in `block (2 levels) in <main>'
Am I missing anything here?
Related
To learn API by using Rails I'm reading this tutorial.
In a part of RSpec test there is a method like this:
spec/support/authentication_helper.rb
module AuthenticationHelper
def sign_in(user)
header('Authorization', "Token token=\"#{user.authentication_token}\", email=\"#{user.email}\"")
end
def create_and_sign_in_user
user = FactoryGirl.create(:user)
sign_in(user)
user
end
alias_method :create_and_sign_in_another_user, :create_and_sign_in_user
end
RSpec.configure do |config|
config.include AuthenticationHelper, type: :api
end
And the test failed by undefined method `header'.
Where is this header method defined?
This is the whole source code of this tutorial.
https://github.com/vasilakisfil/rails_tutorial_api/
spec/apis/users_spec.rb
require 'rails_helper'
describe Api::V1::UsersController, type: :api do
context :show do
before do
create_and_sign_in_user
#user = FactoryGirl.create(:user)
get api_v1_user_path(#user.id), format: :json
end
it 'returns the correct status' do
expect(last_response.status).to eql(200)
end
it 'returns the data in the body' do
body = HashWithIndifferentAccess.new(MultiJson.load(last_response.body))
expect(body[:user][:name]).to eql(#user.name)
expect(body[:user][:updated_at]).to eql(#user.updated_at.iso8601)
end
end
end
StackTrace
1) Api::V1::UsersController show returns the correct status
Failure/Error: create_and_sign_in_user
NameError:
undefined local variable or method `request' for #<RSpec::ExampleGroups::ApiV1UsersController::Show:0x007fcbfec91d60>
# ./spec/support/authentication_helper.rb:4:in `sign_in'
# ./spec/support/authentication_helper.rb:9:in `create_and_sign_in_user'
# ./spec/apis/user_spec.rb:6:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:39:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:38:in `block (2 levels) in <top (required)>'
# -e:1:in `<main>'
2) Api::V1::UsersController show returns the data in the body
Failure/Error: create_and_sign_in_user
NameError:
undefined local variable or method `request' for #<RSpec::ExampleGroups::ApiV1UsersController::Show:0x007fcbfb7cfa28>
# ./spec/support/authentication_helper.rb:4:in `sign_in'
# ./spec/support/authentication_helper.rb:9:in `create_and_sign_in_user'
# ./spec/apis/user_spec.rb:6:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:39:in `block (3 levels) in <top (required)>'
# ./spec/rails_helper.rb:38:in `block (2 levels) in <top (required)>'
# -e:1:in `<main>'
I had to add api_helper.rb to use the methods.
module ApiHelper
include Rack::Test::Methods
def app
Rails.application
end
end
RSpec.configure do |config|
config.include ApiHelper, type: :api #apply to all spec for apis folder
config.include Rails.application.routes.url_helpers, type: :api
end
Here is source code in Github.
https://github.com/vasilakisfil/rails_tutorial_api/blob/008af67e88897a5bcde714ce13d39a26ec70fba7/spec/support/api_helper.rb
In spec/support/auth_helpers.rb, you can try something like this
module AuthHelpers
def authenticate_with_user(user)
request.headers['Authorization'] = "Token token=#{user.token}, email=#{user.email}"
end
def clear_authentication_token
request.headers['Authorization'] = nil
end
end
In Rspec's spec/rails_helper.rb
Rspec.configure do |config|
config.include AuthHelpers, file_path: /spec\/apis/
end
An example test in spec/apis/users_controller_spec.rb:
require 'rails_helper'
describe Api::V1::UsersController, type: :controller do
let(:user) { create(:user) }
context 'signed in' do
before do
authenticate_with_user user
end
it 'does something' # tests here
end
end
Hope it helps!
Edit: Note the type: :controller is important
I'm currently doing rspec testing on two files and I got these failures regarding undefined methods. I need more calcification on how to exactly fixes these errors,Thanks!
Failures:
1) FavoritesController#create creates a favorite for the current user and specified post
Failure/Error: #post = create(:post)
NoMethodError:
undefined method `create' for #<RSpec::ExampleGroups::FavoritesController::Create:0x007fdabc84f7f8>
# /Users/bryanporras/.rvm/gems/ruby-2.2.1/gems/actionpack-4.2.1/lib/action_dispatch/testing/assertions/routing.rb:171:in `method_missing'
# ./spec/controllers/favorites_controller_spec.rb:8:in `block (2 levels) in <top (required)>'
2) FavoritesController#destroy destroys the favorite for the current user and post
Failure/Error: #post = create(:post)
NoMethodError:
undefined method `create' for #<RSpec::ExampleGroups::FavoritesController::Destroy:0x007fdabfe0f3e8>
# /Users/bryanporras/.rvm/gems/ruby-2.2.1/gems/actionpack-4.2.1/lib/action_dispatch/testing/assertions/routing.rb:171:in `method_missing'
# ./spec/controllers/favorites_controller_spec.rb:8:in `block (2 levels) in <top (required)>'
3) VotesController#up_vote adds an up-vote to the post
Failure/Error: sign_in #user
NoMethodError:
undefined method `sign_in' for #<RSpec::ExampleGroups::VotesController::UpVote:0x007fdabfe26fe8>
# /Users/bryanporras/.rvm/gems/ruby-2.2.1/gems/actionpack-4.2.1/lib/action_dispatch/testing/assertions/routing.rb:171:in `method_missing'
# ./spec/controllers/votes_controller_spec.rb:12:in `block (3 levels) in <top (required)>'
4) Vote after_save calls `Post#update_rank` after save
Failure/Error: vote = Vote.new(value: 1, post: post)
NameError:
undefined local variable or method `post' for #<RSpec::ExampleGroups::Vote::AfterSave:0x007fdabdb8b0b0>
# ./spec/models/vote_spec.rb:25:in `block (3 levels) in <top (required)>'
Finished in 0.78639 seconds (files took 2.82 seconds to load)
15 examples, 4 failures, 2 pending
Failed examples:
rspec ./spec/controllers/favorites_controller_spec.rb:14 # FavoritesController#create creates a favorite for the current user and specified post
rspec ./spec/controllers/favorites_controller_spec.rb:24 # FavoritesController#destroy destroys the favorite for the current user and post
rspec ./spec/controllers/votes_controller_spec.rb:8 # VotesController#up_vote adds an up-vote to the post
rspec ./spec/models/vote_spec.rb:24 # Vote after_save calls `Post#update_rank` after save
This is the favorites_controller_spec.rb file:
require 'rails_helper'
describe FavoritesController do
include Devise::TestHelpers
before do
#post = create(:post)
#user = create(:user)
sign_in #user
end
describe '#create' do
it "creates a favorite for the current user and specified post" do
expect( #user.favorites.find_by(post_id: #post.id) ).to be_nil
post :create, { post_id: #post.id }
expect( #user.favorites.find_by(post_id: #post.id) ).not_to be_nil
end
end
describe '#destroy' do
it "destroys the favorite for the current user and post" do
favorite = #user.favorites.where(post: #post).create
expect( #user.favorites.find_by(post_id: #post.id) ).not_to be_nil
delete :destroy, { post_id: #post.id, id: favorite.id }
expect( #user.favorites.find_by(post_id: #post.id) ).to be_nil
end
end
end
and this is the votes_controller_spec.rb file:
require 'rails_helper'
describe VotesController do
include TestFactories
describe '#up_vote' do
it "adds an up-vote to the post" do
request.env["HTTP_REFERER"] = '/'
#user = authenticated_user
#post = associated_post
sign_in #user
expect {
post( :up_vote, post_id: #post.id )
}.to change{ #post.up_votes }.by 1
end
end
end
Check if you have config.include FactoryGirl::Syntax::Methods inside rails_helper.rb
If you have above line in rails_helper.rb file you can always use FactoryGirl.create :user
You didn't probably include include Devise::TestHelpers in specs for VotesController that's why it does not see sign_in method
I was doing the RSpec testing with Rails and it's failing due to the namespaced model that I assigned to its associations of the model.
CLI:
Failures:
1) Blog::BlobsController GET index assigns all blog/blobs as #blobs
Failure/Error: expect(assigns(:blog::blobs)).to eq([blog::blobs])
NoMethodError:
undefined method `blobs' for :blog:Symbol
# ./spec/controllers/blog/blobs_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
spec/controllers/blog/blobs_controller_spec.rb
RSpec.describe Blog::BlobsController, type: :controller do
describe "GET index" do
it "assigns all blog/blobs as #blobs" do
blobs = Blog::Blob.create!
get :index, {}
expect(assigns(:blog::blobs)).to eq([blog::blobs])
end
end
end
routes.rb
# SNIPPED FOR BREVITY...
namespace :blog do
resources :blobs
end
app/controllers/blog/blobs_controller.rb
class Blog::BlobsController < ApplicationController
before_filter :authenticate_user!, except: [:index, :show]
def index
#blobs = Blog::Blob.all
end
def new
#blob = Blog::Blob.new
end
def edit
#blob = Blog::Blob.find(params[:id])
end
def create
#blob = Blog::Blob.new(blob_params)
if #blob.save
redirect_to #blob
else
render 'new'
end
end
def update
#blob = Blog::Blob.find(params[:id])
if #blob.update(blob_params)
redirect_to #blob
else
render 'edit'
end
end
def show
#blob = Blog::Blob.find(params[:id])
end
def destroy
#blob = Blog::Blob.find(params[:id])
#blob.destroy!
redirect_to blog_blobs_path
end
private
def blob_params
params.require(:blob).permit(:title, :body)
end
end
Is there a better way to test this controller with RSpec?
UPDATE:
Failures:
1) Blog::BlobsController GET #index assigns all widgets as #widgets
Failure/Error: expect(assigns(:blobs)).to eq([blob])
TypeError:
no implicit conversion of Symbol into Integer
# /usr/local/rvm/gems/ruby-2.1.5#rails4/gems/mongo-2.0.4/lib/mongo/server_selector.rb:56:in `[]'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/gems/mongo-2.0.4/lib/mongo/server_selector.rb:56:in `get'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/gems/mongo-2.0.4/lib/mongo/client.rb:170:in `read_preference'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/gems/mongo-2.0.4/lib/mongo/collection/view/readable.rb:318:in `default_read'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/gems/mongo-2.0.4/lib/mongo/collection/view/readable.rb:251:in `read'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/gems/mongo-2.0.4/lib/mongo/collection/view/iterable.rb:38:in `each'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/bundler/gems/mongoid-26f67146a7b7/lib/mongoid/query_cache.rb:207:in `each'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/bundler/gems/mongoid-26f67146a7b7/lib/mongoid/contextual/mongo.rb:116:in `each'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/bundler/gems/mongoid-26f67146a7b7/lib/mongoid/contextual.rb:20:in `each'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/bundler/gems/mongoid-26f67146a7b7/lib/mongoid/criteria.rb:48:in `entries'
# /usr/local/rvm/gems/ruby-2.1.5#rails4/bundler/gems/mongoid-26f67146a7b7/lib/mongoid/criteria.rb:48:in `=='
# ./spec/controllers/blog/blobs_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
Updated spec code:
require 'rails_helper'
RSpec.describe Blog::BlobsController, type: :controller do
describe "GET #index" do
it "assigns all blobs as #blobs" do
blob = Blog::Blob.create!
get :index, {}
expect(assigns(:blobs)).to eq([blob])
end
end
end
Your test says:
blobs = Blog::Blob.create!
This is confusing, because blobs is plural, but you're only creating one blob. So start by renaming that to blob. Then expect(assigns(:blog::blobs)).to eq([blog::blobs]) should be expect(assigns(:blobs)).to eq([blob]).
In the index action, you set #blobs = Blog::Blob.all. The assigns correspond to the controller's instance variables. There's no namespacing.
The following rspec test file
require 'spec_helper'
describe EvaluationsController do
render_views
before(:each) do
association_attr
end
describe "'eval_selektor'" do
before(:each) do
#eval_selektor = get :eval_selektor, student_group_id: #student_group
end
it "should be successful" do
#eval_selektor
response.should be_success
end
...
end
...
end
is throwing the following error:
1) EvaluationsController 'eval_selektor' should be successful
Failure/Error: #eval_selektor = get :eval_selektor, student_group_id: #student_group
NoMethodError:
undefined method `student_groups' for nil:NilClass
# ./app/controllers/application_controller.rb:7:in `get_student_group'
# ./spec/controllers/evaluations_controller_spec.rb:14:in `block (3 levels) in <top (required)>'
from this method in the application_controller:
def get_student_group
#user = current_user
#student_group = #user.student_groups.find(params[:student_group_id])
end
At first I thought maybe rspec just wasn't getting passed the method from application_controller, but that's not the case as it can see it in the error. The code works in the browser, and if I put <%= #user %> in the view, it shows the correct user instance. Any ideas why rspec can't read #user?
apneadiving got me started in the right direction, and between this post and this one I got to the correct code:
ApplicationController.any_instance.stub(:current_user).and_return(#user)
I can't figure out why this RSpec test fails. Any advice? I'm new-ish to FactoryGirl, RSpec, and TDD in general.
Controller:
def update
#vendor = current_user.vendors.find(params[:id])
if #vendor.update_attributes(params[:vendor])
redirect_to vendor_path(#vendor)
else
render 'edit'
end
end
Test:
require 'spec_helper'
describe VendorsController do
login_user
before :each do
#user = subject.current_user
#vendor = FactoryGirl.create(:vendor, :user => #user)
end
[...]
describe 'POST update' do
def do_update
post :update, :id => #vendor.id, :vendor => FactoryGirl.attributes_for(:vendor)
end
[...]
it 'should update a given vendor' do
do_update
#vendor.should_receive(:update_attributes).with(FactoryGirl.attributes_for(:vendor))
end
end
end
Factory:
FactoryGirl.define do
factory :vendor do
name 'Widget Vendor'
user
end
end
The Failure:
Failures:
1) VendorsController POST update should update a given vendor
Failure/Error: #vendor.should_receive(:update_attributes).with(FactoryGirl.attributes_for(:vendor))
(#<Vendor:0x007faeb75e77d0>).update_attributes({:name=>"Widget Vendor"})
expected: 1 time
received: 0 times
# ./spec/controllers/vendors_controller_spec.rb:108:in `block (3 levels) in <top (required)>'
Update:
I'm a little closer, now. I changed the test to the following:
it 'should update a given vendor' do
Vendor.any_instance.should_receive(:update_attributes).with(FactoryGirl.attributes_for(:vendor))
do_update
end
And the new error is:
Failures:
1) VendorsController POST update should update a given vendor
Failure/Error: post :update, :id => #vendor.id, :vendor => FactoryGirl.attributes_for(:vendor)
#<Vendor:0x007ff30d765900> received :update_attributes with unexpected arguments
expected: ({:name=>"Widget Vendor"})
got: ({"name"=>"Widget Vendor"})
# ./app/controllers/vendors_controller.rb:33:in `update'
# ./spec/controllers/vendors_controller_spec.rb:98:in `do_update'
# ./spec/controllers/vendors_controller_spec.rb:108:in `block (3 levels) in <top (required)>'
Answer...?
Well, this worked. There has to be a better way of doing this, though:
Vendor.any_instance.should_receive(:update_attributes).with(JSON.parse(FactoryGirl.attributes_for(:vendor).to_json)).and_return(true)
I think you are doing it wrong.
The #vendor object in specs is another one that in your controller, so it doesn't receive "update_attributes" method.
You can try this (rspec 2.5+ probably):
Vendor.any_instance.should_receive(:update_attributes).with(FactoryGirl.attributes_for(:vendor))
Or you can check if object attributes has changed:
expect{
do_update
}.to change(...)
I believe you need to set your expectations before posting the request; otherwise, by the time it hits your expectation the object has already been set. So move do_update after your should_receive line:
it 'should update a given vendor' do
#vendor.should_receive(:update_attributes).with(FactoryGirl.attributes_for(:vendor))
do_update
end
You can use the Hash stringify keys method in rails:
Vendor.any_instance.should_receive(:update_attributes).with(FactoryGirl.attributes_for(:vendor).stringify_keys)