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
Related
I was wondering if anyone can help. I am receiving an error message in the terminal saying Validation failed: Item must exist. I think that happens because FactoryBot is trying to create a Transaction class before the Item class, I think, I have to find a way to make the Item exist first before the Transaction, I tried many different ways, but had no success so far. optional: true is not an option, appreciate any help.
ps: this is my first time I am asking a help in StackOverflow so apologies if something is not right in my post.
Down below you can find 3 files, if you want to see other files let me know.
require "test_helper"
class PurchasesControllerTest < ActionDispatch::IntegrationTest
describe "#index" do
let(:user_id) { Faker::Internet.uuid }
let(:payload) { { user_id:, account_number: 1 } }
let(:purchase_count) { 3 }
before do
WebMock.stub_request(:get, "#{ENV['IDENTITY_SERVICE_URL']}/users/#{user_id}")
.to_return(status: 200, body: user_response(user_id:), headers: jsonapi_headers)
create(:as_asset)
purchase_count.times do
create(:purchase, user_id: 1)
end
end
it "returns successful response" do
get purchases_url, headers: authorization_header(payload)
assert_response :success
assert_match(/"total":#{purchase_count}/, response.body)
end
context "when unauthorized" do
it "returns unauthorized response" do
get purchases_url
assert_response :unauthorized
end
end
end
end
FactoryBot.define do
factory :purchase do
user_id { Faker::Internet.uuid }
for_as_asset
trait :for_gasset do
association :item, factory: :gasset
end
trait :for_as_asset do
association :item, factory: :as_asset
end
after(:create) do |object|
create(:transaction, purchase_id: object.id, purchase_type: "Purchase")
end
end
end
FactoryBot.define do
factory :transaction do
item_id { 4 }
item_type { "AsAsset" }
user_id { Faker::Internet.uuid }
purchase_ref { "Purchase reference" }
end
end
The answer to my question:
Added item_id: object.item_id, item_type: object.item_type inside the block statement of purchase factory.
FactoryBot.define do
factory :purchase do
for_gasset
after(:create) do |object|
create(:transaction, user_id: object.user_id, item_id: object.item_id, item_type: object.item_type, purchase_id: object.id, purchase_type: "Purchase")
end
trait :for_gasset do
association :item, factory: :gasset
end
trait :for_as_asset do
association :item, factory: :as_asset
end
user_id { Faker::Internet.uuid }
end
end
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
I want to test the mailer in my application to make sure it is doing what I want it to do.
class LessonMailer < ApplicationMailer
def send_mail(lesson)
#lesson = lesson
mail(to: lesson.student.email,
subject: 'A lesson has been recorded by your tutor')
end
end
This is my test in the spec/mailers directory
require "rails_helper"
RSpec.describe LessonMailer, :type => :mailer do
describe "lesson" do
let( :student ){ FactoryGirl.create :user, role: 'student', givenname: 'name', sn: 'sname', email: 'test#sheffield.ac.uk' }
let( :lesson ){ FactoryGirl.create :lesson, student_id: 2 }
let( :mail ){ LessonMailer.send_mail( lesson ).deliver_now
it "renders the headers" do
expect(mail.subject).to eq("A lesson has been recorded")
expect(mail.to).to eq(["to#example.ac.uk"])
expect(mail.from).to eq(["no-reply#example.ac.uk"])
end
it "renders the body" do
expect(mail.body.encoded).to match("A lesson form has been recorded")
end
end
end
I want to test that the 'send_mail' method is working the way I want it to however, I am getting this error. How do I go about solving this problem ? Thank you.
NoMethodError:
undefined method `email' for nil:NilClass
# ./app/mailers/lesson_mailer.rb:4:in `send_mail'
So, with FactoryGirl, you just need to instantiate the different objects that you need. Reading your code, it seems clear that a lesson has a student and that students have an email. So go ahead and create everything you need and then call your method. You can do something like this:
# Here's the student factory (for this use case, you'll probably want to make it more general)
FactoryGirl.define do
factory :user do
role 'student'
givenname 'name'
sn 'sname'
email 'test#sheffield.ac.uk'
end
end
# Here's your test
require "rails_helper"
RSpec.describe LessonMailer, :type => :mailer do
describe "lesson" do
let( :student ){ create :student, email: 'test_email#example.com' }
let( :lesson ){ create :lesson, student: student }
let( :mail ){ LessonMailer.send_mail( lesson ) }
it ' ... ' do
...
end
end
end
You'll need to let the test environment know that you want the emails to be delivered to the ActionMailer::Base.deliveries array. To do this, make sure that
config.action_mailer.delivery_method = :test
is set in your config/environments/test.rb
One last thing, I'm not sure if you'll need it, but you might have to call the mailer with, .deliver_now.
Like this:
let( :mail ){ LessonMailer.send_mail( lesson ).deliver_now }
... or it may not send. I can't remember off the top.
Let me know how it goes.
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'm new to RSpec, and trying to get my head around using Factory Girl with associations in controller specs. The difficulty is:
it's necessary to use "attributes_for" in functional tests
attributes_for "elides any associations"
So if I have models like this:
class Brand < ActiveRecord::Base
belongs_to :org
validates :org, :presence => true
end
class Org < ActiveRecord::Base
has_many :brands
end
And a factory like this:
FactoryGirl.define do
factory :brand do
association :org
end
end
This controller spec fails:
describe BrandsController do
describe "POST create with valid params" do
it "creates a new brand" do
expect {
post :create, brand: attributes_for(:brand)
}.to change(Brand, :count).by(1)
end
end
end
(And if I comment out "validates :org, :presence => true" it passes)
There are a number of solutions suggested and I think I have been making simple errors which have meant that I have not been able to get any of them to work.
1) Changing the factory to org_id per a suggestion on this page failed a number of tests with "Validation failed: Org can't be blank"
FactoryGirl.define do
factory :brand do
org_id 1002
end
end
2) Using "symbolize_keys" looks promising. Here and here it is suggested to use code like this:
(FactoryGirl.build :position).attributes.symbolize_keys
I'm not sure how to apply this in my case. Below is a guess that doesn't work (giving the error No route matches {:controller=>"brands", :action=>"{:id=>nil, :name=>\"MyString\", :org_id=>1052, :include_in_menu=>false, :created_at=>nil, :updated_at=>nil}"}):
describe BrandsController do
describe "POST create with valid params" do
it "creates a new brand" do
expect {
post build(:brand).attributes.symbolize_keys
}.to change(Brand, :count).by(1)
end
end
end
Update
I almost got this working with Shioyama's answer below but got the error message:
Failure/Error: post :create, brand: build(:brand).attributes.symbolize_keys
ActiveModel::MassAssignmentSecurity::Error:
Can't mass-assign protected attributes: id, created_at, updated_at
So following this question I changed it to:
post :create, brand: build(:brand).attributes.symbolize_keys.reject { |key, value| !Brand.attr_accessible[:default].collect { |attribute| attribute.to_sym }.include?(key) }
Which worked!
In your solution 2), you have not passed an action to post which is why it is throwing an error.
Try replacing the code in that expect block to:
post :create, brand: build(:brand).attributes.symbolize_keys