I have a book database where books can have different book formats (hardcover, softcover etc).
I have factories with factory_bot.
The following spec just run through with an error - and then when I run it the second time, it worked. I have no idea where I need to start searching....
The error was:
1) BookFormat displays the proper book format for a book with that format
Failure/Error: expect(#book.book_format.name).to eq('Hardcover')
expected: "Hardcover"
got: "Not defined"
Here is the full spec:
require 'rails_helper'
RSpec.describe BookFormat, type: :model do
before(:all) do
#book = create(:hobbit)
#book_format_default = create(:not_defined)
end
it 'displays the proper book format for a book with that format' do
expect(#book.book_format.name).to eq('Hardcover')
end
it 'should reassign to the fallback book_format if their book_format is deleted' do
format = #book.book_format
format.destroy
expect(#book.reload.book_format.id).to eq(#book_format_default.id)
end
it 'should not let the fallback format be deleted' do
format = #book_format_default
format.destroy
expect(format).to be_truthy
end
end
Here is the corresponding factor for the book :hobbit:
factory :hobbit, class: Book do
title { 'The Hobbit' }
year { 1937 }
rating { 5 }
condition { 4 }
synopsis { "<p>#{Faker::Lorem.paragraphs(number: 30).join(' ')}</p>" }
association :book_format, factory: :hardcover
association :user, factory: :me
genres { [ create(:fiction) ] }
after(:build) do |hobbit|
hobbit.cover.attach(
# rubocop:disable Rails/FilePath
io: File.open(Rails.root.join('db', 'sample', 'images', 'cover-1.jpg')),
# rubocop:enable Rails/FilePath
filename: 'cover.jpg',
content_type: 'image/jpeg'
)
end
end
And here are the factories for book_formats:
FactoryBot.define do
factory :not_defined, class: BookFormat do
name { 'Not defined'}
fallback { true }
end
factory :hardcover, class: BookFormat do
name { 'Hardcover' }
end
factory :softcover, class: BookFormat do
name { 'Softcover' }
end
end
Related
I am running into issues with RSpec and FactoryBot and some factories because different tests try to generate the same factories which is not possible due to uniqueness constraints - and also logically wrong.
I would like to somehow centrally create the factory objects, so that each test has access to them instead of having "before..." or "let..." blocks / directives in different test files.
How to achieve that?
Example problem:
I have these factories:
FactoryBot.define do
factory :office_tk, class: Shelf do
name { 'Office tk' }
association :user, factory: :me
end
end
and
FactoryBot.define do
factory :hobbit, class: Book do
title { 'The Hobbit' }
year { 1937 }
rating { 5 }
condition { 4 }
synopsis { "<p>#{Faker::Lorem.paragraphs(number: 30).join(' ')}</p>" }
association :book_format, factory: :hardcover
association :user, factory: :me
genres { [ create(:fiction) ] }
association :shelf, factory: :office_tk
end
end
and
FactoryBot.define do
factory :me, class: User do
name { 'tkhobbes' }
email { 'me#example.com' }
password { 'password' }
password_confirmation { 'password' }
admin { true }
activated { true }
activated_at { Time.zone.now }
end
end
When I try to run this test - it complains because it tries to create :me twice (which is not possible, as each user / email can only exist once - apart from the fact that :me should be the same object for both the book and the shelf).
RSpec.describe Shelf, type: :model do
before(:all) do
#book = create(:hobbit)
end
it 'shows the right shelf for a book' do
expect(#book.shelf.name).to eq('Office tk')
end
end
I am testing model using rspec and factory Girl
My model
class Currency < ActiveRecord::Base
has_many :countries
validates :name, presence: true
validates :name, uniqueness: true
before_destroy :safe_to_delete
def safe_to_delete
countries.any? ? false : true
end
end
My factory girl
FactoryGirl.define do
factory :currency, class: 'Currency' do
sequence(:name) { |i| "Currency-#{i}" }
end
end
My currency_spec.rb is
require 'rails_helper'
describe Currency , type: :model do
let(:currency) { create(:currency) }
let(:currency1) { create(:currency) }
let(:country) { create(:country) }
describe 'associations' do
subject {currency}
it { should have_many(:countries) }
end
describe 'validations' do
subject {currency}
it { should validate_presence_of(:name) }
it { should validate_uniqueness_of(:name) }
end
describe 'method save_to_delete' do
context 'case false' do
before { country.update_column(:currency_id, currency.id) }
subject { currency.destroy }
it { is_expected.to be_falsy }
end
context 'case true' do
before { country.update_column(:currency_id, currency1.id) }
subject { currency.destroy }
it { is_expected.to be_truthy }
end
end
end
The error is:
Failure/Error: let(:currency) { create(:currency) }
ActiveRecord::RecordInvalid:
A validação falhou: Name não está disponível
Even though I disable the presence and uniqueness validations in the model, the problem continues
Who can help me
Did you properly create the migration to include the name on currencies at database level?
Because I created the migration here locally and the tests passed.
Please take a look on the below code.
It is what I did locally and is working here!
1. Migration
file: db/migrate/2021XXXXXXXXXX_create_currencies.rb
class CreateCurrencies < ActiveRecord::Migration
def change
create_table :currencies do |t|
t.string :name
end
end
end
2. Model
app/models/currency.rb
class Currency < ActiveRecord::Base
has_many :countries
validates :name, presence: true, uniqueness: true # Can be oneliner ;)
before_destroy :safe_to_delete
def safe_to_delete
countries.empty? # Much simpler, right? ;)
end
end
3. Factory
spec/factories/currency.rb
FactoryGirl.define do
factory :currency do
sequence(:name) { |i| "Currency-#{i}" }
end
end
4. Tests
spec/models/currency_spec.rb
require 'rails_helper'
describe Currency, type: :model do
let(:currency1) { create(:currency) }
let(:currency2) { create(:currency) }
let(:country) { create(:country) }
describe 'associations' do
subject { currency1 }
it { should have_many(:countries) }
end
describe 'validations' do
subject { currency1 }
it { should validate_presence_of(:name) }
it { should validate_uniqueness_of(:name) }
end
describe 'when the currency is being deleted' do
context 'with countries associated' do
before { country.update_column(:currency_id, currency1.id) }
subject { currency1.destroy }
it { is_expected.to be_falsy }
end
context 'with no countries associated' do
before { country.update_column(:currency_id, currency2.id) }
subject { currency1.destroy }
it { is_expected.to be_truthy }
end
end
end
Test Execution
Finally, the tests should work correctly with the above setup!
spec/models/currency_spec.rb
rspec spec/models/currency_spec.rb
D, [2021-03-06T03:31:03.446070 #4877] DEBUG -- : using default configuration
D, [2021-03-06T03:31:03.449482 #4877] DEBUG -- : Coverband: Starting background reporting
.....
Top 5 slowest examples (0.10688 seconds, 11.4% of total time):
Currency when the currency is being deleted with countries associated should be falsy
0.04095 seconds ./spec/models/currency_spec.rb:23
Currency associations should have many countries
0.03529 seconds ./spec/models/currency_spec.rb:10
Currency when the currency is being deleted with no countries associated should be truthy
0.01454 seconds ./spec/models/currency_spec.rb:29
Currency validations should validate that :name cannot be empty/falsy
0.00812 seconds ./spec/models/currency_spec.rb:15
Currency validations should validate that :name is case-sensitively unique
0.00797 seconds ./spec/models/currency_spec.rb:16
Finished in 0.93948 seconds (files took 8.04 seconds to load)
5 examples, 0 failures
All tests passed ✅
I'm writing some tests using FactoryGirl and Rspec.
spec/factories/students.rb:
FactoryGirl.define do
factory :student do
end
factory :student_with_profile_and_identity, class: 'Student' do
after(:create) do |student|
create(:profile, profileable: student)
create(:student_identity, student: student)
end
end
end
spec/factories/profiles.rb:
FactoryGirl.define do
factory :profile do
birthday { Faker::Date.birthday(15, 150) }
sequence(:email) { |i| "profile_#{i}#email.com" }
first_name { Faker::Name.first_name }
last_name { Faker::Name.first_name }
password { Faker::Internet.password(6, 72, true, true) }
end
end
spec/factories/student_identities.rb:
FactoryGirl.define do
factory :student_identity do
provider { ['facebook.com', 'google.com', 'twitter.com'].sample }
uid { Faker::Number.number(10) }
end
end
spec/requests/authorizations_spec.rb:
require 'rails_helper'
RSpec.describe 'Authorizations', type: :request do
describe 'POST /v1/authorizations/sign_in' do
let!(:student) { create(:student_with_profile_and_identity) }
context 'when the request is valid' do
subject do
post '/v1/authorizations/sign_in',
params: credentials
end
context "user signs up via social network" do
let(:credentials) do
{
authorization: {
student: {
profile_attributes: {
email: student.profile.email
},
student_identities_attributes: {
provider: student.student_identities[0].provider,
uid: student.student_identities[0].uid
}
}
}
}
end
it 'returns an authentication token' do
subject
p "1 student.profile.inspect #{student.profile.inspect}"
expect(json['token']).to(be_present)
end
end
context 'when the user has already an account' do
let(:credentials) do
{
authorization: {
email: student.profile.email,
password: student.profile.password
}
}
end
it 'returns an authentication token' do
p "2 student.profile.inspect #{student.profile.inspect}"
subject
expect(json['token']).to(be_present)
end
end
end
end
end
Almost all tests are passing... the problem is that:
It's creating a new student in every context. I'd expect the let!(:student) { ... } to be something like "singleton", in other words, once it's created/defined here let!(:student) { create(:student_with_profile_and_identity) } it won't be called anymore.
Ex: the logs are like this:
"1 student.profile.inspect #<Profile id: 1, email: \"profile_1#email.com\", profileable_type: \"Student\", profileable_id: 1>"
"2 student.profile.inspect #<Profile id: 2, email: \"profile_2#email.com\", profileable_type: \"Student\", profileable_id: 2>"
While I'd expect the instances to be the same.
Am I missing something?
In RSpec, let and let! are the same thing, except that let is lazy and let! is eager:
Use let to define a memoized helper method. The value will be cached across multiple calls in the same example but not across examples.
Note that let is lazy-evaluated: it is not evaluated until the first time the method it defines is invoked. You can use let! to force the method's invocation before each example.
If you want something to persist through all examples, you can use a before hook...before(:context) sounds like it might be what you're wanting. You might be able to setup a helper method that memoizes in a before block, to avoid having to use an instance variable everywhere (per this comment):
def student
#student ||= create(:student_with_profile_and_identity)
end
before(:context) do
student # force student creation
end
I've got a before_destroy callback that looks like this:
class Component < ActiveRecord::Base
has_many :documents, through: :publications
def document_check
if documents.exists?
errors[:documents] << 'cannot exist'
return true
else
return false
end
end
The test looks like this:
describe '#document_check' do
let(:document) { create(:document) }
let(:component) { create(:component) }
context 'with documents' do
before do
document.components << component
end
specify { expect(component.errors).to include(:document, 'cannot exist') }
specify { expect(component.document_check).to eq true }
end
context 'without documents' do
before do
document.components = []
end
specify { expect(component.document_check).to eq false }
end
end
I want it to raise the error if a component is in a document, but I can't seem to be able to write it correctly. The second test passes, the first doesn't:
Diff:
## -1,2 +1,2 ##
-[:document, "cannot exist"]
+[]
What am I doing wrong?
How is document_check being invoked? If manually (as you're 2nd tests seem to suggest) then you also need to invoke it for the first specify.
That is:
specify { component.document_check; expect(component.errors).to include(:document, 'cannot exist') }
That's horrible syntax, but you need to invoke the method before you can check the errors on it.
Here's the callback:
def document_check
return unless documents.present?
errors.add(:article, 'in use cannot be deleted')
false
end
And here's the passing test for it.
describe '#document_check' do
let(:subject) { create(:component) }
let(:document) { create(:document) }
let(:count) { Component.size }
before do
document.components << subject
subject.send :document_check
end
context 'with documents raises error' do
specify do
expect(subject.errors[:article]).to be_present
end
end
context 'with documents raises correct error' do
specify do
expect(subject.errors[:article]).to include(
'in use cannot be deleted')
end
end
context 'with documents prevents deletion' do
specify do
expect { subject.destroy }.to_not change(Component, :count)
end
end
end
Took ages but it's worth it.
Here is the method I am testing:
class User < ActiveRecord::Base
has_many :sports, :through => :user_sports, order: "user_sports.created_at", class_name: "Sport"
has_many :user_sports
def primary_sport
return nil if user_sports.blank?
user_sports.primary_only.first.sport
end
end
User Factory;
FactoryGirl.define do
sequence(:email) do |n|
"user#{n}#example.com"
end
factory :user do
email
first_name Faker::Name.first_name
last_name Faker::Name.last_name
password "password"
password_confirmation "password"
agreed_to_age_requirements true
username "testing123"
state "AL"
city_id 201
school_id 20935
handedness "Left"
customer_id { "#{rand(1000)}" }
sports {[create(:sport)]}
after(:create) do |user, elevator|
user.subscriptions << create(:subscription)
user.roles << create(:role)
end
end
factory :athlete, class: "Athlete", parent: :user do
type "Athlete"
recruit_year "2016"
end
end
Here is my test:
require 'spec_helper'
describe User do
describe "associations" do
it { should have_and_belong_to_many(:roles) }
it { should belong_to(:account_type) }
it { should belong_to(:primary_sport).class_name("Sport") }
it { should belong_to(:school) }
it { should belong_to(:city) }
it { should belong_to(:hometown) }
it { should have_many(:social_actions) }
it { should have_one(:invitation) }
it { should have_many(:authorizations) }
it { should belong_to(:user_type) }
it { should have_and_belong_to_many(:positions).class_name "SportPosition" }
it { should have_many(:sports).through(:user_sports) }
it { should have_many(:user_sports) }
it { should have_many :contributorships }
it { should have_many(:managed_athletes).through(:contributorships) }
it { should have_and_belong_to_many(:subscriptions) }
end
describe "nested attributes" do
it { should accept_nested_attributes_for(:user_sports) }
it { should accept_nested_attributes_for(:subscriptions) }
end
describe "validations" do
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
it { should allow_value("test#test.com").for(:email) }
it { should_not allow_value("test.com").for(:email) }
end
describe "instance methods" do
before :each do
#user = create(:user, sports: [])
#school_admin_role = create(:role, name: "School Admin")
#contributor_role = create(:role, name: "Contributor")
end
describe "#my_athletes_path" do
it "returns a school admin path if the user has the role of School Admin" do
#user.roles << #school_admin_role
#user.my_athletes_path.should eq school_admin_athletes_path
end
it "returns a school admin path if the user has the role of Contributor" do
#user.roles << #contributor_role
#user.my_athletes_path.should eq contributor_dashboard_path
end
it "returns nil if the user has no Contributor or School Admin role" do
#user.my_athletes_path.should be_nil
end
end
describe "#first_time_login?" do
it "will evalute true if the user has logged in only once" do
#user.sign_in_count = 1
#user.save
#user.first_time_login?.should be_true
end
end
describe "#confirmation_required?" do
it "returns false" do
#user.confirmation_required?.should be_false
end
end
describe "#primary_sport", focus: true do
context "when user has no primary sport" do
it "returns nil" do
#user.primary_sport.should be_nil
end
end
context "when user has a primary sport" do
it "returns sport object" do
#user.sports << create(:sport)
#user.primary_sport.should eq #user.sports.first
end
end
end
end
end
This is the error I am receiving:
Failure/Error: #user.primary_sport.should eq #user.sports.first
NoMethodError:
undefined method sport for nil:NilClass
This is because when the user_sport association is created in the User Factory, the primary column is being set to false. Not sure how to fix this. Any help is greatly appreciated! Also, sorry for the ignorance on the TDD front, Im a newb
Couldn't you just add the following to your after(:create) block in the User factory:
us = user.user_sports.first
us.primary = true
us.save
That would ensure the association gets the primary flag.