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
Related
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
I'm trying to get in the habit of writing specs, however, this is becoming increasingly frustrating.
Assume I have two simple models: User and Story. Each model uses a belongs_to relation. Each model uses a validates :foo_id, presence: true as well.
However, FactoryGirl is creating multiple records.
FactoryGirl.define do
factory :user do
email "foo#bar.com"
password "foobarfoobar"
end # this creates user_id: 1
factory :story do
title "this is the title"
body "this is the body"
user # this creates user_id: 2
end
end
This simple test fails:
require 'rails_helper'
describe Story do
let(:user) { FactoryGirl.create(:user) }
let(:story) { FactoryGirl.create(:story) }
it 'should belong to User' do
story.user = user
expect(story.user).to eq(user)
end
end
What am I missing here? I cannot build a Story factory without a User, yet I need it to be just one User record.
The values you define for each attribute in a factory are only used if you don't specify a value in your create or build call.
user = FactoryGirl.create(:user)
story = FactoryGirl.create(:story, user: user)
When doing something like this you can do:
let(:story) { FactoryGirl.create(:story, user: user) }
Or maybe you can only let the story variable and do:
let(:story) { FactoryGirl.create(:story, user: user) }
let(:user) { User.last}
Yes, it is a feature of factory girl to create the associated user when you create the story.
You can avoid it like this:
require 'rails_helper'
describe Story do
let(:story) { FactoryGirl.create(:story) }
let(:user) { story.user }
it 'should belong to User' do
story.user.should eq user
end
end
This example is setup to be trivially true, but you get the point.
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
Given I have the following class
class listing > ActiveRecord::Base
attr_accessible :address
belongs_to :owner
validates :owner_id, presence: true
validates :address, presence: true
end
Is there a way I can get away with not having to keep associating an owner before I save a listing in my tests in /spec/models/listing_spec.rb, without making owner_id accessible through mass assignment?
describe Listing do
before(:each) do
#owner = Factory :owner
#valid_attr = {
address: 'An address',
}
end
it "should create a new instance given valid attributes" do
listing = Listing.new #valid_attr
listing.owner = #owner
listing.save!
end
it "should require an address" do
listing = Listing.new #valid_attr.merge(:address => "")
listing.owner = #owner
listing.should_not be_valid
end
end
No need to use factory-girl (unless you want to...):
let(:valid_attributes) { address: 'An Address', owner_id: 5}
it "creates a new instance with valid attributes" do
listing = Listing.new(valid_attributes)
listing.should be_valid
end
it "requires an address" do
listing = Listing.new(valid_attributes.except(:address))
listing.should_not be_valid
listing.errors(:address).should include("must be present")
end
it "requires an owner_id" do
listing = Listing.new(valid_attributes.except(:owner_id))
listing.should_not be_valid
listing.errors(:owner_id).should include("must be present")
end
There is if you use factory-girl
# it's probably not a good idea to use FG in the first one
it "should create a new instance given valid attributes" do
listing = Listing.new #valid_attr
listing.owner = #owner
listing.property_type = Factory(:property_type)
listing.save!
end
it "should require an address" do
# But here you can use it fine
listing = Factory.build :listing, address: ''
listing.should_not be_valid
end
it "should require a reasonable short address" do
listing = Factory.build :listing, address: 'a'*245
listing.should_not be_valid
end
I hate to be the voice of dissent here, but you shouldn't be calling save! or valid? at all in your validation spec. And 9 times out of 10, if you need to use factory girl just to check the validity of your model, something is very wrong. What you should be doing is checking for errors on each of the attributes.
A better way to write the above would be:
describe Listing do
describe "when first created" do
it { should have(1).error_on(:address) }
it { should have(1).error_on(:owner_id) }
end
end
Also, chances are you don't want to be checking for the presence of an address, you want to check that it is not nil, not an empty string, and that it is not longer than a certain length. You'll want to use validates_length_of for that.
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 }