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
Related
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
Background:
I am trying to create a FactoryBot object which is related with has_one/belongs_to
User has_one Car
Car has_one Style
Style has an attribute {style_number:"1234"}
Question
My controller references user, user has_one Car, Car has_one Style, and I need to set these values within FactoryBot.
How do I create a User, who also has a Car object, that has a Style object?
I read the documentation https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md
However, I am not understanding how they recommend doing this. Figured out, I need to nest the three objects, but confused on the syntax.
Controller
before_action :authenticate_user!
before_action :set_steps
before_action :setup_wizard
include Wicked::Wizard
def show
#user = current_user
#form_object = form_object_model_for_step(step).new(#user)
render_wizard
end
private
def set_steps
if style_is_1234
self.steps = car_steps.insert(1, :style_car)
else
self.steps = car_steps
end
end
def style_is_1234
if params.dig(:form_object, :style_number)
(params.dig(:form_object, :style_number) & ["1234"]).present?
else
(current_user.try(:car).try(:style).try(:style_number) & ["1234"]).present?
end
end
def car_steps
[:type,:wheel, :brand]
end
Rspec Test
Factory :User
FactoryBot.define do
factory :user, class: User do
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
password { "somepassword" }
password_confirmation { "some password"}
end
end
Before method
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryBot.create(:user)
sign_in user
Test
User needs to be signed in and User.car.style.style_number needs to be set to "1234"
context "Requesting with second step CarStyle" do
it "should return success" do
get :show, params: { :id => 'car_style' }
expect(response.status).to eq 200
end
end
Currently this test fails because User.Car.Style.style_number is not set to "1234".
Trial 1 (https://github.com/thoughtbot/factory_bot_rails/issues/232)
FactoryBot.define do
factory :user, class: User do
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
password { "somepassword" }
password_confirmation { "some password"}
car
end
end
FactoryBot.define do
factory :car, class: Car do
make { "Holden" }
model { "UTE" }
end
end
FactoryBot.define do
factory :style, class: Style do
color { "blue" }
for_car
trait :for_car do
association(:styable, factory: :car)
end
end
end
Error from trail 1
SystemStackError:
stack level too deep
Trail 2
I tried srng's recommendation
EDIT: For a polymorphic association try;
FactoryBot.define do
factory :car, class: Car do
make { "Holden" }
model { "UTE" }
association :stylable, factory: :style
end
end
and got error:
ActiveRecord::RecordInvalid: Validation failed: Stylable must exist
I think this is a rails 5 issue. https://github.com/rails/rails/issues/24518
However, I would like to keep my code with the adding the optional:true. Any way to do this?
Trail 3
FactoryBot.define do
factory :car, class: Car do
make { "Holden" }
model { "UTE" }
after(:create) do |car|
create(:style, stylable: car)
end
end
end
Tried Srng's second recommendation and although it worked for him, I got a slightly different error:
ActiveRecord::RecordInvalid:
Validation failed: User must exist
In order to create dependent Factories you have to create a factory for each model, and then just add the dependent Model name to your factory, ie.
FactoryBot.define do
factory :user, class: User do
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
password { "somepassword" }
password_confirmation { "some password"}
car
end
end
FactoryBot.define do
factory :car, class: Car do
make { "Holden" }
model { "UTE" }
style
end
end
FactoryBot.define do
factory :style, class: Style do
color { "blue" }
end
end
EDIT:
Relevant code;
# Factories
FactoryBot.define do
factory :user, class: User do
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
password { "somepassword" }
password_confirmation { "some password"}
after(:create) do |user|
user.car ||= create(:car, :user => user)
end
end
end
factory :style, class: Style do
style_number { "Blue" }
end
factory :car, class: Car do
name { "Holden" }
trait :style do
association :stylable, factory: :style
end
end
#models
class Car < ApplicationRecord
has_one :style, as: :styleable
end
class Style < ApplicationRecord
belongs_to :styleable, polymorphic: true
belongs_to :car
end
# Migrations - The belongs_to is the only important one
class CreateStyles < ActiveRecord::Migration[5.2]
def change
create_table :styles do |t|
t.string :style_number
t.belongs_to :stylable, polymorphic: true
t.timestamps
end
end
end
class CreateCars < ActiveRecord::Migration[5.2]
def change
create_table :cars do |t|
t.string :name
t.timestamps
end
end
end
There can be an another way of attaining this using a transient block in the Factory.
Hope the below snippet may help you to explore in new way.
Note: This is not tested.
## To Create a user in test case
# create(:user) # defaults to 1234 style number
# create(:user, car_style_number: 5678)
DEFAULT_STYLE_NUMBER = 1234
FactoryBot.define do
factory :user do
transient do
car_style_number { DEFAULT_STYLE_NUMBER }
end
first_name { "John" }
last_name { "Doe" }
email { Faker::Internet.email }
after(:create) do |user, evaluator|
user.car = create(:car, car_style_number: evaluator.car_style_number, user: user)
end
end
end
FactoryBot.define do
factory :car do
transient do
car_style_number { DEFAULT_STYLE_NUMBER }
end
make { "Holden" }
model { "UTE" }
after(:create) do |car, evaluator|
car.style = create(:style, style_number: evaluator.car_style_number, car: car)
end
end
end
FactoryBot.define do
factory :style do
style_number { DEFAULT_STYLE_NUMBER }
end
end
I read the documentation of FactoryBot here: https://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md
I have users and roles and it is a has_and_belongs_to_many relation. I have tried many steps from the documentation to setup this relation but nothing works.
First, I tried this technique:
FactoryBot.define do
factory :role do
name { "marketing" }
factory :admin_role do
name { "admin" }
end
end
end
FactoryBot.define do
factory :user, aliases: [:marketing] do
email { 'marketing#mysite.io' }
password { '123456' }
password_confirmation { '123456' }
association :role
end
end
But it gives me:
NoMethodError: undefined method `role=' for #<User:0x007f9743449198>
Second, I tried this technique:
FactoryBot.define do
factory :role do
name { "marketing" }
factory :admin_role do
name { "admin" }
end
end
end
FactoryBot.define do
factory :user, aliases: [:marketing] do
email { 'marketing#mysite.io' }
password { '123456' }
password_confirmation { '123456' }
role
end
end
But again I get this error:
NoMethodError: undefined method `role=' for #<User:0x007f9743449198>
Third, I tried this technique of pluralizing the relation:
FactoryBot.define do
factory :role do
name { "marketing" }
factory :admin_role do
name { "admin" }
end
end
end
FactoryBot.define do
factory :user, aliases: [:marketing] do
email { 'marketing#mysite.io' }
password { '123456' }
password_confirmation { '123456' }
role
end
end
I get this error:
ArgumentError: Trait not registered: roles
Yet when I read the documentation, it suggests I can use these methods. So what am I doing wrong?
Not sure if this will work but since a user can have many roles you probably need to add a role as a single array item
FactoryBot.define do
factory :user, aliases: [:marketing] do
email { 'marketing#mysite.io' }
password { '123456' }
password_confirmation { '123456' }
roles { [ role ] }
end
end
Or
FactoryBot.define do
factory :user, aliases: [:marketing] do
email { 'marketing#mysite.io' }
password { '123456' }
password_confirmation { '123456' }
roles { [ create(role) ] }
end
end
The reason for your error is you can't call #user.role but you can call #user.roles with HABTM relation.
I am doing the following rspec test to test my '#clubs' method
context "#clubs" do
it "returns the users associated Clubs" do
club = create(:club)
user = club.host
expect(user.clubs).to contain(club)
end
end
The method in my User model:
class User < ActiveRecord::Base
has_many :host_clubs, :class_name => 'Club', :inverse_of => :host
has_and_belongs_to_many :volunteer_clubs, :class_name => 'Club', :inverse_of => :volunteers
def clubs
[host_clubs, volunteer_clubs].flatten
end
end
When I run the test and use p club.host, it returns the user as expected, however I cannot see why calling user.clubs => [] returns an empty array. Here is the factory for context.
factory :host, class: User do |f|
f.first_name { Faker::Name.first_name }
f.last_name { Faker::Name.last_name }
f.email { Faker::Internet.email }
f.password { Faker::Internet.password }
f.role { "host" }
f.onboarded_at { Date.today }
after(:create) do |host|
host.confirm_email_address
end
end
factory :club, class: Club do |f|
f.venue_type { "Primary school" }
f.name { Faker::Company.name }
f.address_1 { Faker::Address.street_address }
f.city { Faker::Address.city }
host
end
Can anyone give me a heads up to why it may not be returning the record?
It's worth noting that I am adding this test after the method was created. In the console the method behaves as expected.
(Making my comment as an answer with additional information)
Try user.reload before expecting the results in the test solves this issue.
This is because of club object is created independently from user. This is generally happens in rspec tests.
I have three factories that i want to DRY up. They look like this:
factory :sequenced_stamps_by_years, class: Stamp do
...
sequence(:day_date) { |n| n.years.ago }
end
factory :sequenced_stamps_by_months, class: Stamp do
...
sequence(:day_date) { |n| n.months.ago }
end
factory :sequenced_stamps_by_weeks, class: Stamp do
...
sequence(:day_date) { |n| n.weeks.ago }
end
How can i dry this up? I want to be able to create them something like this:
FactoryGirl.create_list(:sequenced_stamps_by_x, 4, x: "weeks") ## <- So that i can decide whether I want weeks, days, years, or months ago.
Is this possible?
If you don't favor the inheritance approach, there is an alternative using a parameter. Basically:
factory :stamps do
ignore do
interval :years # possible values => :years, :months, :weeks
end
sequence(:date_date) { |n| n.send(interval).ago }
# rest of attributes here
end
Now you can do:
FactoryGirl.create(:stamps, :interval => :months)
or
FactoryGirl.create(:stamps)
which defaults to years.
All this you can find in Factory Girl transient attributes
Factories can inherit from other factories. Therefore you can do something like:
factory :stamps do
# common attributes here
.....
factory: sequenced_stamps_by_years do
sequence(:day_date) { |n| n.years.ago }
end
factory: sequenced_stamps_by_months do
sequence(:day_date) { |n| n.months.ago }
end
factory: sequenced_stamps_by_weeks do
sequence(:day_date) { |n| n.weeks.ago }
end
end