I've started to use Rspec and right now I wrote several successfully worked and pretty difficult tests. But as I need more practice I did refactor of these tests few times.
I not found an answer for my question in Rspec's documentation and that's why I here.
The question is about directive let that provides an ability to return some objects by first call and not only, you know.
My current part of rspec code is:
RSpec.describe 'Users', type: :request do
describe 'profiles' do
context 'should be visible by' do
it 'related company managers' do
company = create(:company)
sign_in create(:manager, :company, company: company) # Pay attention on this
get user_path(create(:member, :company, company: company)) # this
expect(response).to have_http_status 200
end
it 'related company owners' do
company = create(:company)
sign_in create(:owner, :company, company: company) # this
get user_path(create(:member, :company, company: company)) # and this
expect(response).to have_http_status 200
end
end
end
end
There are only two example of 63 that are under the User's spec, but they are enough. I want to refactor the code to use let which will define a method with parameters, like that:
RSpec.describe 'Users', type: :request do
let(:member) do |entity_name = :company, entity = create(entity_name)|
create :member, entity_name, entity_name => entity
end
let(:manager) do |entity_name = :company, entity = create(entity_name)|
create :manager, entity_name, entity_name => entity
end
let(:owner) do |entity_name = :company, entity = create(entity_name)|
create :owner, entity_name, entity_name => entity
end
describe 'profiles' do
context 'should be visible by' do
it 'related company managers' do
company = create(:company)
sign_in manager(:company, company) # Become more readable here
get user_path(member(:company, company)) # here
expect(response).to have_http_status 200
end
it 'related company owners' do
company = create(:company)
sign_in owner(:company, company) # here
get user_path(member(:company, company)) # and here.
expect(response).to have_http_status 200
end
end
end
end
Right after the refactor the Guard says:
1) Users profiles should be visible by related company managers
Failure/Error: sign_in manager(:company, company) # Become more readable here
ArgumentError:
wrong number of arguments (given 2, expected 0)
# ./spec/requests/users_spec.rb:38:in `block (4 levels) in <top (required)>'
From the memorized_helpers.rb of Rspec core I saw:
def let(name, &block)
# We have to pass the block directly to `define_method` to
# allow it to use method constructs like `super` and `return`.
raise "#let or #subject called without a block" if block.nil?
raise(
"#let or #subject called with a reserved name #initialize"
) if :initialize == name
MemoizedHelpers.module_for(self).__send__(:define_method, name, &block)
# Apply the memoization. The method has been defined in an ancestor
# module so we can use `super` here to get the value.
if block.arity == 1
define_method(name) { __memoized.fetch_or_store(name) { super(RSpec.current_example, &nil) } }
else
define_method(name) { __memoized.fetch_or_store(name) { super(&nil) } }
end
end
It looks like let's blocks should be defined with parameters. Or not?
I haven't enough experience to determine it and I would be glad to find it out.
I've found possible solution: return a lambda and call it in place
RSpec.describe 'Users', type: :request do
let(:member) do
->(entity_name = :company, entity = create(entity_name)) do
create :member, entity_name, entity_name => entity
end
end
let(:manager) do
->(entity_name = :company, entity = create(entity_name)) do
create :manager, entity_name, entity_name => entity
end
end
let(:owner) do
->(entity_name = :company, entity = create(entity_name)) do
create :owner, entity_name, entity_name => entity
end
end
describe 'profiles' do
context 'should be visible by' do
it 'related company managers' do
company = create(:company)
sign_in manager.(:company, company) # Pay attention on the dot here
get user_path(member.(:company, company)) # here
expect(response).to have_http_status 200
end
it 'related company owners' do
company = create(:company)
sign_in owner.(:company, company) # here
get user_path(member.(:company, company)) # and here.
expect(response).to have_http_status 200
end
end
end
end
This is not exactly that I want, but seems like.
I think you want something like:
RSpec.describe 'Users', type: :request do
describe 'profiles' do
let(:request) { get user_path(user) }
let(:user) { create(:member, :company, company: company) }
let(:company) { create(:company) }
shared_examples_for 'has visibility to user' do
before { sign_in employee }
it do
request
expect(response).to have_http_status(:success)
end
end
context 'manager' do
let(:employee) { create(:manager, company: company) }
it_behaves_like 'has visibility to user'
end
context 'owner' do
let(:employee) { create(:owner, company: company) }
it_behaves_like 'has visibility to user'
end
end
end
Similarly, you should be able to do something like:
it_behaves_like 'has visibility to user' do
let(:employee) { create(:manager, company: company) }
end
with shared examples. I suggest checking out http://www.betterspecs.org/.
(Aside, it's confusing to have a factory named :member and not :user since the GET request is for user_path; I recommend renaming it.)
The another solution is creation of helpers. For example, create a file in spec/support/users_helper.rb.
module UsersSpecHelper
def member(entity = :company)
create :member, *entity(entity)
end
def manager(entity = :company)
create :manager, *entity(entity)
end
def owner(entity = :company)
create :owner, *entity(entity)
end
private
def entity(entity)
if entity.is_a? Symbol
[entity, entity => create(entity)]
else
name = entity.class.to_s.downcase.to_sym
[name, name => entity]
end
end
end
RSpec.configure do |config|
config.include UsersSpecHelper, type: :request
end
Uncomment in spec/rails_helper.rb the line:
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
and then it can be used even better:
RSpec.describe 'Users', type: :request do
describe 'profiles' do
context 'should be visible by' do
it 'managers of related company' do
company = create(:company)
sign_in manager(company) # Changes are here
get user_path(member(company)) # here
expect(response).to have_http_status 200
end
it 'owners of related company' do
company = create(:company)
sign_in owner(company) # here
get user_path(member(company)) # and here.
expect(response).to have_http_status 200
end
end
end
end
Related
I am trying and failing to test a controller for variable assignment of the belongs_to objects. These are controller tests and there are a number of areas I could really appreciate with some help on, namely
Should I be writing such tests here and in this way.
If so how could i get it working.
Code as below:
Company.rb
class Company < ApplicationRecord
belongs_to :user
has_many :employees, inverse_of: :company
has_many :quotes, inverse_of: :company
end
Quote.rb
class Quote < ApplicationRecord
belongs_to :company
end
Employee.rb
class Employee < ApplicationRecord
belongs_to :company
end
Company has a controller with usual CRUDs, Quote has a controller with Show and Index, Employee does not have a controller. Companies#create creates all three objects and redirect_to's to Quotes#show which renders various attrs from all three models.
companies_controller.rb #create
def create
#company = current_user.companies.new(company_params)
if #company.save
#quote = #company.quotes.last
#employees = #company.employees.all
redirect_to company_quote_url(#company, #quote, #employees), notice: 'Quote request created'
else
render :new
end
end
quotess_controller.rb #show
def show
#company = Company.find(params[:company_id])
#quote = #company.quotes.find(params[:id])
#employees = #company.employees.all
end
I have a Factory Girl factory set up for eahc of the models:
Companies.rb
FactoryGirl.define do
factory :company do
sequence(:co_name) { |n| "Acme Co #{n}" }
co_number "06488522"
postcode "al1 1aa"
industry :financial_services
factory :company2 do
end
factory :company3 do
end
end
end
Quotes.rb
FactoryGirl.define do
factory :quote do
lives_overseas true
payment_frequency :monthly
factory :quote2 do
end
factory :quote3 do
end
end
end
Employees.rb
FactoryGirl.define do
factory :employee1, class: Employee do
first_name "MyString"
last_name "MyString"
email "test#test.com"
gender "MyString"
date_of_birth "2000-06-20"
salary 10000
factory :employee2 do
end
factory :employee3 do
end
end
end
And I am trying to write controller tests for Quote#show and to test the assignment of the three objects, i.e.; #company, #quote & #employees to the relataive variables. Code so far as below:
quotes_controller_spec.rb
require 'rails_helper'
RSpec.describe QuotesController, type: :controller do
let(:user) {FactoryGirl.create(:user) }
let(:company) { FactoryGirl.create(:company, user: user) }
let(:employee1) { FactoryGirl.create(:employee1, company: company) }
let(:employee2) { FactoryGirl.create(:employee2, company: company) }
let(:employee3) { FactoryGirl.create(:employee3, company: company) }
let(:quote) { FactoryGirl.create(:quote, company: company) }
describe "GET #show" do
it "returns http success" do
get :show, params: { company_id: company.id, id: quote.id, , employee_id: employee1.id }
expect(response).to have_http_status(:success)
end
it "assigns requested quote to #quote" do
get :show, params: { company_id: company.id, id: quote.id, employee1.id: employee1.id } #, employee_id: employee1.id
expect(assigns(:quote)).to eq(quote) # passes fine
expect(assigns(:company)).to eq(company) # passes fine
expect(assigns(:employee1)).to eq(employee1) # fails
end
end
end
I get an error as below:
Failures:
1) QuotesController GET #show assigns requested quote to #quote
Failure/Error: expect(assigns(:employee1)).to eq(employee1)
expected: #<Employee id: 1, first_name: "MyString", last_name: "MyString", email: "test#test.com", gender: "m",...alary: 10000, company_id: 178, created_at: "2017-07-01 11:21:27", updated_at: "2017-07-01 11:21:27">
got: nil
(compared using ==)
# ./spec/controllers/quotes_controller_spec.rb:28:in `block (3 levels) in <top (required)>'
When i run the app and use params.inspect in Quote#show template after a Company#create these are the params that are passed:
<ActionController::Parameters {"controller"=>"quotes", "action"=>"show", "company_id"=>"109", "id"=>"109", "format"=>"#<Employee::ActiveRecord_AssociationRelation:0x007fc2694a07f8>"} permitted: false>
I feel like there are a few core things I am not getting right here;
I need somehow to declare the associations within Factory Girl
My tests should somehow be testing the presence of a collection and its assignment to the #employees variable in Quotes#show, not assignment of just one employee record, which is what I'm trying, and failing, to do above.
I am unsure about whether I am crossing 'lines of separation' that perhaps ought to be present because I am testing on other model objects (Company, Quote and Employee) created in Companies#create and rendered in Quotes#show.
Any help and or guidance appreciated. The afternoon reading and googling leaves me still at a loss as to how I can get my testing strategy right here and the syntax correct for it to work properly. Incidentally all works in the app just fine, I'd just like to be able to test the assignment of the correct object in this Quotes#show method. Thanks.
The answer to part 2 of this question, re. testing assignment of a collection, in the context of my code was either:
expect(assigns(:employees)).to include(employee1)
or
expect(assigns(:employees)).to eq([employee1])
Feedback on parts 1 and 3 of this question still sought.
Thanks
I'm creating some test to test a controller and model. When I use FactoryGirl to create fake data I'm getting errors that the User (which the record belongs to) does not exist.
Here is my model composition.rb
class Composition < ActiveRecord::Base
belongs_to :user
belongs_to :group
validates :name, presence: true, uniqueness: {scope: :user_id}
end
Here is my FactoryGirl file composition.rb
require 'faker'
FactoryGirl.define do
factory :composition do
name { Faker::Name.name }
description { Faker::Lorem.words }
import_composition { Faker::Boolean.boolean }
import_composition_file { Faker::File.file_name('path/to') }
end
end
This is my the RSpec test that I have until this far
require 'rails_helper'
describe CompositionsController do
before(:each) do
#user = FactoryGirl.create(:user)
#group = FactoryGirl.create(:group)
sign_in #user
#composition = Composition.new(FactoryGirl.create(:composition), user_id: #user.id, group_id: #group.id)
end
describe "GET #index" do
it "renders the index template" do
get :index
expect(assigns(:composition).to eq(#composition))
expect(response).to render_template("index")
end
end
end
Right now I'm getting an error: Validation failed: User must exist, Group must exist
When I don't user FactoryGirl to create a record everything works fine.
Does any body have an suggestion why it's failing?
You don't need to pass FactoryGirl as a param to Model
#composition = FactoryGirl.create(:composition, user: #user, group: #group)
If you don't want to create the record but just want it to initialize, use build instead of create
#composition = FactoryGirl.build(:composition, user: #user, group: #group)
I have the following (simplified) Rails Concern:
module HasTerms
extend ActiveSupport::Concern
module ClassMethods
def optional_agreement
# Attributes
#----------------------------------------------------------------------------
attr_accessible :agrees_to_terms
end
def required_agreement
# Attributes
#----------------------------------------------------------------------------
attr_accessible :agrees_to_terms
# Validations
#----------------------------------------------------------------------------
validates :agrees_to_terms, :acceptance => true, :allow_nil => :false, :on => :create
end
end
end
I can't figure out a good way to test this module in RSpec however - if I just create a dummy class, I get active record errors when I try to check that the validations are working. Has anyone else faced this problem?
Check out RSpec shared examples.
This way you can write the following:
# spec/support/has_terms_tests.rb
shared_examples "has terms" do
# Your tests here
end
# spec/wherever/has_terms_spec.rb
module TestTemps
class HasTermsDouble
include ActiveModel::Validations
include HasTerms
end
end
describe HasTerms do
context "when included in a class" do
subject(:with_terms) { TestTemps::HasTermsDouble.new }
it_behaves_like "has terms"
end
end
# spec/model/contract_spec.rb
describe Contract do
it_behaves_like "has terms"
end
You could just test the module implicitly by leaving your tests in the classes that include this module. Alternatively, you can include other requisite modules in your dummy class. For instance, the validates methods in AR models are provided by ActiveModel::Validations. So, for your tests:
class DummyClass
include ActiveModel::Validations
include HasTerms
end
There may be other modules you need to bring in based on dependencies you implicitly rely on in your HasTerms module.
I was struggling with this myself and conjured up the following solution, which is much like rossta's idea but uses an anonymous class instead:
it 'validates terms' do
dummy_class = Class.new do
include ActiveModel::Validations
include HasTerms
attr_accessor :agrees_to_terms
def self.model_name
ActiveModel::Name.new(self, nil, "dummy")
end
end
dummy = dummy_class.new
dummy.should_not be_valid
end
Here is another example (using Factorygirl's "create" method" and shared_examples_for)
concern spec
#spec/support/concerns/commentable_spec
require 'spec_helper'
shared_examples_for 'commentable' do
let (:model) { create ( described_class.to_s.underscore ) }
let (:user) { create (:user) }
it 'has comments' do
expect { model.comments }.to_not raise_error
end
it 'comment method returns Comment object as association' do
model.comment(user, "description")
expect(model.comments.length).to eq(1)
end
it 'user can make multiple comments' do
model.comment(user, "description")
model.comment(user, "description")
expect(model.comments.length).to eq(2)
end
end
commentable concern
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments, as: :commentable
end
def comment(user, description)
Comment.create(commentable_id: self.id,
commentable_type: self.class.name,
user_id: user.id,
description: description
)
end
end
and restraunt_spec may look something like this (I'm not Rspec guru so don't think that my way of writing specs is good - the most important thing is at the beginning):
require 'rails_helper'
RSpec.describe Restraunt, type: :model do
it_behaves_like 'commentable'
describe 'with valid data' do
let (:restraunt) { create(:restraunt) }
it 'has valid factory' do
expect(restraunt).to be_valid
end
it 'has many comments' do
expect { restraunt.comments }.to_not raise_error
end
end
describe 'with invalid data' do
it 'is invalid without a name' do
restraunt = build(:restraunt, name: nil)
restraunt.save
expect(restraunt.errors[:name].length).to eq(1)
end
it 'is invalid without description' do
restraunt = build(:restraunt, description: nil)
restraunt.save
expect(restraunt.errors[:description].length).to eq(1)
end
it 'is invalid without location' do
restraunt = build(:restraunt, location: nil)
restraunt.save
expect(restraunt.errors[:location].length).to eq(1)
end
it 'does not allow duplicated name' do
restraunt = create(:restraunt, name: 'test_name')
restraunt2 = build(:restraunt, name: 'test_name')
restraunt2.save
expect(restraunt2.errors[:name].length).to eq(1)
end
end
end
Building on Aaron K's excellent answer here, there are some nice tricks you can use with described_class that RSpec provides to make your methods ubiquitous and make factories work for you. Here's a snippet of a shared example I recently made for an application:
shared_examples 'token authenticatable' do
describe '.find_by_authentication_token' do
context 'valid token' do
it 'finds correct user' do
class_symbol = described_class.name.underscore
item = create(class_symbol, :authentication_token)
create(class_symbol, :authentication_token)
item_found = described_class.find_by_authentication_token(
item.authentication_token
)
expect(item_found).to eq item
end
end
context 'nil token' do
it 'returns nil' do
class_symbol = described_class.name.underscore
create(class_symbol)
item_found = described_class.find_by_authentication_token(nil)
expect(item_found).to be_nil
end
end
end
end
I am testing the controllers with RSpec, FactoryGirls.
It is my factories.rb
FactoryGirl.define do
factory :user do |user|
user.sequence(:name) { Faker::Internet.user_name }
user.email Faker::Internet.email
user.password "password"
user.password_confirmation "password"
end
factory :article do
user
title Faker::Lorem.sentence(5)
content Faker::Lorem.paragraph(20)
end
end
How can i create an article of the user here
And this is articles_controller_spec
describe ArticlesController do
let(:user) do
user = FactoryGirl.create(:user)
user.confirm!
user
end
describe "GET #index" do
it "populates an array of articles of the user" do
#how can i create an article of the user here
sign_in user
get :index
assigns(:articles).should eq([article])
end
it "renders the :index view" do
get :index
response.should render_template :index
end
end
end
The older version, instead of traits, is this:
describe ArticlesController do
..
describe "GET #index" do
it "populates an array of articles of the user" do
article = FactoryGirl.create(:article, :user => user)
sign_in user
get :index
assigns(:articles).should eq([article])
end
..
end
describe ArticlesController do
let(:user) do
user = FactoryGirl.create(:user)
user.confirm!
user
end
describe "GET #index" do
it "populates an array of articles of the user" do
#how can i create an article of the user here
sign_in user
get :index
assigns(:articles).should eq([article])
end
it "renders the :index view" do
get :index
response.should render_template :index
end
it "assign all atricles to #atricles" do
get :index
assigns(:atricles).your_awesome_test_check # assigns(:articles) would give you access to instance variable
end
end
end
you can specify an User factory with articles already
FactoryGirl.define do
factory :user do |user|
user.sequence(:name) { Faker::Internet.user_name }
user.email Faker::Internet.email
user.password "password"
user.password_confirmation "password"
end
factory :article do
user
title Faker::Lorem.sentence(5)
content Faker::Lorem.paragraph(20)
end
trait :with_articles do
after :create do |user|
FactoryGirl.create_list :article, 2, :user => user
end
end
end
then in your controller test
FactoryGirl.create :user, :with_articles # => returns user with 2 articles
UPDATE
i think you want to see all articles per user.. if thats the case use
get :index, {:id => user.id}
that way you look for the user and get all articles in your controller
#user = User.find(params[:id]);
#articles = #user.articles
if thats not the case then just doing
#articles = Article.all
after using the trait :with_articles should display at least 2 Articles
you can test this with a simply asserting like
expect(#article.size).to eq(2)
I was wondering if i could have some feedbacks with the controller spec bellow. In fact i'm new when writing specs and controller's spec are way different from model's spec ! So i'm wondering if i may not go in the wrong direction...
subjects_controller.rb
def show
#subject = Subject.find(params[:id])
if #subject.trusted?(current_user)
#messages = #subject.messages
else
#messages = #subject.messages.public
#messages = #messages + #subject.messages.where(:user_ids => current_user.id)
#messages.uniq!
end
# sort the list
#messages = #messages.sort_by(&:created_at).reverse
if !#subject.company.id == current_user.company.id
redirect_to(subjects_path, :notice => "Invalid subject")
end
end
subjects_controller_spec.rb
require 'spec_helper'
describe SubjectsController do
before(:each) do
#subject = mock_model(Subject)
end
context "for signed users" do
before(:each) do
#current_user = sign_in Factory(:user)
end
context "GET #show" do
before(:each) do
Subject.stub!(:find, #subject).and_return(#subject)
end
context "when current_user is trusted" do
before(:each) do
messages = []
company = mock_model(Company)
#subject.should_receive(:trusted?).and_return(true)
#subject.should_receive(:messages).and_return(messages)
#subject.should_receive(:company).and_return(company)
end
it "should render success" do
get :show, :id => #subject
response.should be_success
end
end
context "when current_user is not trusted" do
before(:each) do
messages = []
company = mock_model(Company)
#subject.should_receive(:trusted?).and_return(false)
#subject.should_receive(:messages).and_return(messages)
messages.should_receive(:public).and_return(messages)
#subject.should_receive(:messages).and_return(messages)
messages.should_receive(:where).and_return(messages)
#subject.should_receive(:company).and_return(company)
end
it "should render success" do
get :show, :id => #subject
response.should be_success
end
end
context "when subject's company is not equal to current_user's company" do
# I have no idea of how to implement ==
end
end
end
end
Factories.rb
Factory.define :user do |u|
u.first_name 'Test User' #
u.username 'Test User' #
u.surname 'TheTest' #
u.email 'foo#foobar.com' #
u.password 'please' #
u.confirmed_at Time.now #
end
As far as I can tell you're on the right path. The basic idea is to completely isolate your controller code from model and view in these tests. You appear to be doing that--stubbing and mocking model interaction.
Don't write RSpec controller specs at all. Use Cucumber stories instead. Much easier, and you get better coverage.