I do not write tests very well and I have some trouble using instance variable from Application Controller for another controllers in test.
In Rails I had a pretty simple controller action.
def index
#cities = City.all
#starred_cities = #cities.where(starred: true)
end
For this action I have a test:
RSpec.describe CitiesController, :type => :controller do
let(:city) { create(:city) }
describe 'GET #index' do
let(:cities) { create_list(:city, 2) }
before { get :index }
it 'populates an array of all cities' do
expect(assigns(:cities)).to match_array(cities)
end
it 'renders index view' do
expect(response).to render_template :index
end
end
end
In application I need to get a country by domain name and set globally for all controllers. I add to ApplicationController before_action method like this:
before_action :get_country
def get_country
country_slugs = {en: 'usa', ru: 'russia', es: 'spain'}
current_country_slug = country_slugs[I18n.locale]
#country = Country.find_by_slug(current_country_slug)
end
And now I can get cities in my controller only for current country:
def index
#cities = #country.cities
#starred_cities = #cities.where(starred: true)
end
Now I have some trouble because my controller test fails with exception:
Failures:
1) CitiesController GET #index populates an array of all cities
Failure/Error: #cities = #country.cities
NoMethodError:
undefined method `cities' for nil:NilClass
# ./app/controllers/cities_controller.rb:5:in `index'
# ./spec/controllers/cities_controller_spec.rb:9:in `block (3 levels) in <top (required)>'
2) CitiesController GET #index renders index view
Failure/Error: #cities = #country.cities
NoMethodError:
undefined method `cities' for nil:NilClass
# ./app/controllers/cities_controller.rb:5:in `index'
# ./spec/controllers/cities_controller_spec.rb:9:in `block (3 levels) in <top (required)>'
Please help, what should I do to combine such instance variable and make an association on it?
You have to set up all associations used in the testcase properly, in your case the country with assigned cities is missing (thus nil.cities is called) or mock the methods to return the objects as AR would return them, like:
RSpec.describe CitiesController, :type => :controller do
describe '#index' do
let(:cities) { double('cities') }
let(:starred_cities) { double('starred_cities') }
let(:country) { double('country', cities: cities) }
before do
allow(cities).to receive(:where).with(starred: true).and_return(starred_cities)
allow(Country).to receive(:find_by_slug).and_return(country)
get :index
end
it 'populates an array of all cities' do
expect(assigns(:cities)).to match_array(cities)
end
it 'renders index view' do
expect(response).to render_template :index
end
end
end
Mocking can be quite usefull if you know what you are doing to prevent hitting the db (slow!) since AR is already quite tested well. But also can let you write passing tests although your implementation has bugs, so use it wisely.
Related
Using Rails 5.1.4, Ruby 2.4.1, rspec
Scenario:
In article destroy allow only user current_ma_user with role "a,m"
Then:
Check if current_ma_user.role = "a,m"
or current_ma_user own article (#article.user)
So I create current_ma_user as hash as well as user.
Then call role to check what is user[role ]
Problems:
How to add new method to hash.
How to pass that hash.method from rspec controller_spec to controller.
Failures:
1) ArticlesController DELETE #destroy destroys the requested article
Failure/Error: delete :destroy, params: {id: article.to_param}, session: valid_session, :current_ma_user.role => "a,m"
NoMethodError:
undefined method `role' for :current_ma_user:Symbol
# ./spec/controllers/articles_controller_spec.rb:172:in `block (4 levels) in <top (required)>'
# ./spec/controllers/articles_controller_spec.rb:171:in `block (3 levels) in <top (required)>'
This is the gist
articles_controller_spec.rb:
require 'rails_helper'
RSpec.describe ArticlesController, type: :controller do
class Hash #patch to temp pass problem 1
def role
"a,m" #Hard Code, need to call user["role"] need code
end
end
user = {}
user["uid"] = "admin"
user["provider"] = "Facebook"
user["email"] = "1.0#kul.asia"
user["role"] = "a,m"
current_ma_user = user
describe "DELETE #destroy" do
it "destroys the requested article" do
article = Article.create! valid_attributes
expect {
delete :destroy, params: {id: article.to_param}, session: valid_session
}.to change(Article, :count).by(-1)
end
it "redirects to the articles list" do
article = Article.create! valid_attributes
delete :destroy, params: {id: article.to_param}, session: valid_session
expect(response).to redirect_to(articles_url)
end
end
end
Controller:
class ArticlesController < ApplicationController before_action :load_article, only: [:show, :destroy]
def destroy
if current_ma_user.role.upcase.split(',').include?("A") || current_ma_user == #article.user
#if current_ma_user == #article.user
#article.destroy
end
redirect_to :action=>'index' end
private
def load_article
#article = Article.find(params[:id]) end
end
Updated with line number:
Updated debug to show value of current_ma_user in .spec and controller
This is where your error is coming from (in your controller):
if current_ma_user.role.upcase.split(',').include?("A") || current_ma_user == #article.user
Suggested Solutions
Where is current_ma_user defined in the controller? (if it’s not assigned, then it needs to be assigned before you call the role method on the current_ma_user variable.
Try that and see how it goes.
Do something like this:
current_ma_user = User.find( params[:user_id])
Now you seem to want to pass something into the params hash. Remember to white list whatever you decide to pass into params. Whether it is user id or roles id etc, or a roles string.
When writing your tests, pass in the approrpiate values to the params hash. If you are passing in a user_id in your test, then you will have to make sure that a user is created in the test.
delete :destroy, {:id => article.id.to_s, :user_id => #current_ma_user.id }, session: valid_session
also perhaps in your spec file, in your test, put the current_ma_user in a before filter and make it an instance variable so it will be accessible to all your tests:
before(:each) do
#current_ma_user = user.create( <--- create the user with the
appropriate attributes here --->)
end
Warning: Untested
I just typed it into the stack overflow editor.
I have a controller in application:
class CartsController < ApplicationController
def show
#cart = Cart.find(session[:cart_id])
#products = #cart.products
end
end
and i wrote some initial spec to test response using rspec
RSpec.describe CartsController, type: :controller do
describe 'GET #show' do
before do
get :show
end
it { expect(response.status).to eq(200) }
it { expect(response.headers["Content-Type"]).to eql("text/html; charset=utf-8")}
it { is_expected.to render_template :show }
end
end
Now I am going to test show method logic and should write some expectation like:
it 'should be products in current cart' do
end
but I have no idea how to pass cart.id to the session hash
Update! I am trying to write product instances what will be associated with current cart:
let(:products_list){FactoryGirl.build_list(:product, cart_id: session[:cart_id])}
let(:cart){FactoryGirl.build(:cart)}
...
it 'should be products in current cart' do
session[:cart_id] = cart.id
expect(assigns(:products)).to eq([products_list])
end
but got an error:
CartsController GET #show should be products in current cart
Failure/Error: let(:cart){FactoryGirl.build(:cart)}
ArgumentError:
Trait not registered: products
# ./spec/controllers/carts_controller_spec.rb:6:in `block (3 levels) in <top (required)>'
# ./spec/controllers/carts_controller_spec.rb:15:in `block (3 levels) in <top (required)>'
Something still going wrong
you can set the session in your controller test
it 'should be products in current cart' do
session[:cart_id] = 10
get :show
end
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.
I'm trying to write a simple RSpec test for "UsersController" that tests the index method.
The code for the controller index method looks as follows:
# GET /users
# GET /users.json
def index
#users = User.all
respond_to do |format|
format.html # index.html.erb
format.json { render json: #users }
end
end
I am trying to test that the "all" method is called and that the index view is rendered. Here is my Rspec code for that:
require 'spec_helper'
describe UsersController do
describe 'get index', :type => :controller do
before :each do
#fake_users = [double('user1'), double('user2')]
end
it 'should call the model method that retrieves all Users' do
User.should_receive(:all).once.and_return(#fake_users)
get :index
end
describe 'after valid search' do
before :each do
User.stub(:all).and_return(#fake_users)
get :index
end
it 'should select the index template for rendering' do
response.should render_template('index')
end
it 'should make the users results available to that template' do
assigns(:users).should == #fake_users
end
end
end
end
However, this fails the "get index" test with the following message:
Failure/Error: User.should_receive(:all).once.and_return(#fake_users)
(<User(id: integer, firstname: string, lastname: string, username: string, email: string, password_digest: string, created_at: datetime, updated_at: datetime) (class)>).all(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/controllers/users_controller_spec.rb:9:in `block (3 levels) in <top (required)>'
Can anyone shed any light on what I'm doing wrong?
The problem was caused by the UsersController requiring a log in first. So the controller looked like:
class UsersController < AuthenticatedController
So the path wasn't accessible to RSpec.
Here is the rspec code for testing show in customers controller:
it "'show' should be successful" do
#category = Factory(:category)
#sales = Factory(:user)
#customer = Factory(:customer, :category1_id => category.id, :sales_id => sales.id)
category = mock_model('Category')
sales = mock_model('User')
customer = mock_model(Category, :sales_id => sales.id, :category1_id => category.id)
get 'show' , :id => customer.id
response.should be_success
end
Here is the error in rspec:
CustomersController GET customer page 'show' should be successful
Failure/Error: get 'show' , :id => customer.id
ActiveRecord::RecordNotFound:
Couldn't find Customer with id=1003
# c:in `find'
# ./app/controllers/customers_controller.rb:59:in `show'
# ./spec/controllers/customers_controller_spec.rb:50:in `block (3 levels) in <top (required)>'
The rspec test passes with the real record created by Factory (see #ed in rspec code)
What's wrong with the mock? Thanks.
The spec is failing inside the controller's action which doesn't know anything about your mocks unless you told it explicitly.
Add this to your spec, before the get statement.
Customer.should_receive(:find).and_return(customer)