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
Related
I have a small Rails application with a database that holds different Languages. Each of these Languages is subject to change, and i want to keep track of their changes.
I do this by creating an Audit record when the object is changed. This Audit has a has_many :languages, through: :audit_language_couplings field which validates the relationship.
class Audit < ApplicationRecord
belongs_to :audit_type
has_many :audit_language_couplings
has_many :languages, through: :audit_language_couplings
validates_presence_of :audit_type, :admin_id, :date, :before
end
class Language < ApplicationRecord
has_many :audit_language_couplings
has_many :audits, through: :audit_language_couplings
validates_presence_of :iso_code, :display_name, :keyboard_layout, :flag_url, :luis_app_identification, :luis_authorized_key, :luis_location
end
The audits are created by calling the create_audit() method in the LanguagesController when the PUT, DELETE or POST method is called. I also have a /languages/:id/audits endpoints that returns all the audits for the given language in JSON.
The create_token() method:
def create_audit(type, admin_id)
#language.audits.create(
audit_type_id: type,
admin_id: admin_id,
date: Time.now.to_date,
before: #language.to_s # TODO: Use the to-be-created to_json() or to_s() method instead.
)
end
This is also the nature of my problem (i think).
I am currently testing my API with RSpec and Factory bot requests. When i create or update a Language in the tests, no Audits are being created for some reason. But i know the code works, because it works when i do it manually with postman in my dev environment.
FactoryBot.define do
factory :language do
iso_code { Faker::Address.country_code }
display_name { Faker::Address.country }
keyboard_layout { Faker::Internet.url }
flag_url { Faker::Internet.url }
luis_app_identification { Faker::Lorem.characters(5) }
luis_authorized_key { Faker::Lorem.characters(5) }
luis_location { Faker::Lorem.characters(5) }
end
end
I have currently structured my tests like this:
describe 'POST /admin/language' do
let(:valid_attributes) do
{
payload: {
iso_code: 'en-US',
display_name: 'English (US)',
keyboard_layout: 'QWERTY',
flag_url: '/public/images/en-US.png',
luis_app_identification: 'test',
luis_authorized_key: 'test',
luis_location: 'test'
}
}
end
context 'when the request is valid' do
before { post '/admin/languages', params: valid_attributes, headers: token_header }
it 'creates a language' do
expect(json['iso_code']).to eq('en-US')
end
it 'returns status code 201' do
expect(response).to have_http_status(201)
end
context 'an audit should be made for the change' do
before { get "/admin/languages/#{language_id}/audits", headers: token_header }
it 'creates 1 audit' do
expect(json.size).to eq 1
end
it 'is an audit of type 1 [ADD]' do
expect(json[0]['audit_type_id']).to eq 1
end
end
end
context 'when the request is invalid' do
before do
post '/admin/languages', headers: token_header, params:
{
payload: {
display_name: 'English (US)',
keyboard_layout: 'QWERTY',
flag_url: '/public/images/en-US.png',
luis_app_identification: 'test',
luis_authorized_key: 'test',
luis_location: 'test'
}
}
end
it 'returns status code 422' do
expect(response).to have_http_status(422)
end
it 'returns a validation failure message' do
expect(response.body).to match(/Validation failed: Iso code can't be blank/)
end
end
end
The test where i check the audits fails, because 0 audits are returned when running the code with RSpec.
I think i am doing something wrong with the factories, but i am not sure, please let me know!
Cheers
Hard to say exactly what's going on based on the code provided, but one thing you can try is changing your create_audit method to:
def create_audit(type, admin_id)
#language.audits.create!(
Adding the ! (bang) to the create method will raise an exception if for some reason the create fails, which should show up in your RSpec logs. This should at least help you get to the source of the problem.
What you need to check is that when the post happens, that the number of Audit instances increases by one. I'd do this like this:
subject { post '/admin/languages', params: valid_attributes, headers: token_header }
it 'creates an audit'
expect { subject }.to change { Audit.count }.by(1)
end
At the moment you test that there is one audit (without checking how many there were before the spec) and via an indirect method (calling another API endpoint after the first).
Both Pennycracker and ReggieB's answers were correct, or actually part of the bigger picture.
The part about the create!() shebang lead me to the actual issue. The factories did not create an AuditType on which the Audit was depending.
ReggieB suggested that my test setup was flawed, because i was testing a nested request.
I have opted to use an altered version of his suggestion, one which better fits my current setup:
context 'when the request is valid' do
before { post '/admin/languages', params: valid_attributes, headers: token_header(admin_id: admin.id) }
it 'returns status code 201' do
expect(response).to have_http_status(201)
end
context 'the audit system has to be updated' do
it 'creates 1 audit' do
expect(Audit.all.size).to eq(1)
end
it 'should have type 1 [ADD]' do
expect(Audit.first.audit_type.id).to eq(1)
end
end
end
The database cleaner gem cleans the database after each example, so checking for the first Audit works the same as expecting a change.
Thanks everyone for the help.
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 am learning how to test on rails from this tutorial.
On one part of the tutorial, it shows how to write invalid_attribute test:
require 'rails_helper'
RSpec.describe ContactsController, type: :controller do
describe "POST #create" do
context "with valid attributes" do
it "create new contact" do
post :create, contact: attributes_for(:contact)
expect(Contact.count).to eq(1)
end
end
context "with invalid attributes" do
it "does not create new contact" do
post :create, contact: attributes_for(:invalid_contact)
expect(Contact.count).to eq(0)
end
end
end
end
I don't understand where :contact and :invalid_contact point to.
Does :contact points to Contact class? It seems like it from FactoryGirl's gh. If so, then how can I create :invalid_contact since there is no :invalid_contact class?
I have tried post :create, contact: attributes_for(:contact, :full_name => nil) but it still fails.
spec/factories/contacts.rb:
FactoryGirl.define do
factory :contact do
full_name { Faker::Name.name }
email { Faker::Internet.email }
phone_number { Faker::PhoneNumber.phone_number }
address { Faker::Address.street_address }
end
end
First test, with valid attributes pass. On model, there is presence validation validates_presence_of :full_name, :email, :phone_number, :address. What do I add in order to pass "with invalid attributes" test?
The factory will use the class with the same name. So your :contact factory will use the Contact class. You can create a new factory for the invalid contact by specifying the class to use.
factory :invalid_contact, class: Contact do
full_name nil
end
It's also possible to use traits to avoid having two different factories.
FactoryGirl.define do
factory :contact do
full_name { Faker::Name.name }
email { Faker::Internet.email }
phone_number { Faker::PhoneNumber.phone_number }
address { Faker::Address.street_address }
trait :invalid do
full_name nil
end
end
end
Then use it with attributes_for(:contact, :invalid)
The tutorial you link to says:
Following the spec above, write a spec that uses invalid attributes to
create a new contact. This spec should check that the contact is not
created.
So you need to figure out how to test for :invalid_contact using the example for :contact.
You can just add a let in your spec:
Use let to define a memoized helper method. The value will be cached
across multiple calls in the same example but not across examples.
Source: https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/helper-methods/let-and-let
Then your controller spec would look like this:
...
let(:invalid_contact) { create(:contact, name: nil) }
context "with invalid attributes" do
it "does not create new contact" do
post :create, contact: attributes_for(invalid_contact)
expect(Contact.count).to eq(0)
end
end
...
this way #post action params are picked up from invalid_contact
or as #fanta suggested in comments, you can add a trait to your factory. I prefer my method because other people looking at your code will know why invalid_contact should be invalid without looking at the :contacts factory
I run test, display error.
Failures:
1) ContractsController POST #create with valid attributes redirects to payment page
Failure/Error: #proposal = Proposal.find(params[:proposal_id])
ActiveRecord::RecordNotFound:
Couldn't find Proposal with 'id'=
require 'rails_helper'
describe ContractsController do
login_client
describe 'POST #create' do
let(:proposal) { create(:proposal) }
let(:contract) { create(:contract) }
context 'with valid attributes' do
it 'redirects to payment page' do
post :create, contract: attributes_for(:contract)
expect(response).to redirect_to payment_new_path
end
end
end
end
factory girls:
FactoryGirl.define do
factory :contract do
sequence(:title) { |n| "translation#{n}" }
amount 150
additional_information 'X' * 500
due_date { 21.days.from_now }
proposal
client
contractor
end
end
FactoryGirl.define do
factory :proposal do
description text
amount 150
project
user
end
end
I'm sure you're getting this error because of the use of FactoryGirl#attributes_for. Why? When you use the attributes_for method, it returns a non-persisted hash attribute for the resource. The thing about attributes_for however is that it doesn't honor association, which makes sense(in order to keep FactoryGirl ORM agnostic). A suggested way around this is to use or define a custom strategy:
build(:contract).attributes
Find more useful references here
My controller spec fails because Factory Girl seems to be creating non-unique Users even though I sequence the User attributes that need to be unique.
The Errors
1) TopicsController POST #create when topic is invalid should render new
Failure/Error: let(:invalid_topic) {Factory.build :invalid_topic}
ActiveRecord::RecordInvalid:Validation failed: Email has already been taken, Username has already been taken
2) TopicsController POST #create when topic is valid should redirect to show
Failure/Error: let(:valid_topic) {Factory.build :topic}
ActiveRecord::RecordInvalid:
Validation failed: Email has already been taken, Username has already been taken
The Controller Spec (RSpec)
describe "POST #create" do
let(:valid_topic) {Factory.build :topic}
let(:invalid_topic) {Factory.build :invalid_topic}
context "when topic is invalid" do
it "should render new" do
post :create, :topic => invalid_topic
response.should render_template(:new)
end
end
context "when topic is valid" do
it "should redirect to show" do
post :create, :topic => valid_topic
response.should redirect_to(topic_path(assigns(:topic)))
end
end
end
The Factories
Factory.define :user do |f|
f.sequence(:username) { |n| "foo#{n}"}
f.password "password"
f.password_confirmation { |u| u.password}
f.sequence(:email) { |n| "foo#{n}#example.com"}
end
Factory.define :topic do |f|
f.name "test topic"
f.association :creator, :factory => :user
f.forum_id 1
end
Why isn't Factory Girl sequencing the User attributes when I use Factory.create :topic?
rake db:test:prepare seemed to fix the problem.
Not sure why, though. The schema hadn't been changed.
Please, consider using database_cleaner gem. One was designed specifically to fulfill the purpose of cleaning up database between test runs.
This post explains pretty much everything.
You should consider deleting all topics by hand in the end of the test. Of course, it is not number one solution but worked out for me real great.
after(:all) { Topic.delete_all }