I'm new to TDD, RSpec and factories, and trying to understand how to test that each User's phone number attribute is unique. To do so, I'm trying to use a sequence in my User factory. I'm not having much luck with the following:
FactoryGirl.define do
factory :user do
number = 123456789
sequence(:phone_number) {|n| (number + n).to_s }
end
end
Any thoughts on the best way to accomplish this? Also, what kind of test would make sense for something like this where ultimately I would want to add the following validation to the user model to make such a test pass?
validates :phone_number, :uniqueness => true
Thanks!
Try using a lambda with a random 10 digit number:
phone_number { rand(10**9..10**10) }
Try this:
FactoryGirl.define do
sequence :phone_number do |n|
"123456789#{n}"
end
factory :user do
phone_number
end
end
and in order to test your validation use this in your user_spec
it { should validate_uniqueness_of(:phone_number) }
To complete #westonplatter answer, in order to start at 0 000 000 000, you can use String#rjust:
FactoryGirl.define do
factory :user do
sequence(:phone_number) {|n| n.to_s.rjust(10, '0') }
end
end
Example:
> 10.times { |n| puts n.to_s.rjust(10, '0') }
0000000000
0000000001
0000000002
0000000003
0000000004
0000000005
0000000006
0000000007
0000000008
0000000009
While the random solution works, you have a small chance of not getting a unique number. I think you should leverage the FactoryGirl sequence.
We can start at, 1,000,000,000 (100-000-000) and increment up. Note: This only gives you 98,999,999,999 unqiue phone numbers, which should be sufficient. If not, you have other issues.
FactoryGirl.define do
sequence :phone_number do |n|
num = 1*(10**8) + n
num.to_s
end
factory :user do
phone_number
end
end
Related
I use FactoryBot's sequence method to generate unique values, which work great in tests.
Example:
FactoryBot.define do
factory :my_factory do
sequence(:label) { |n| "Label #{n}" }
end
end
However, if I try to generate records in my dev environment, I hit lots of errors because the sequence counter has reset. I would love to set it manually, something like set_sequence(123456), is anything like that possible?
Sequence accepts a seed value parameter so something like this should work
FactoryBot.define do
factory :my_factory do
sequence(:label, 123456) { |n| "Label #{n}" }
end
end
You could now set the start value in your configuration
# config/environments/production.rb
Rails.application.configure do
config.x.factory_bot.sequence_seed_value = 1234
end
# config/environments/test.rb
Rails.application.configure do
config.x.factory_bot.sequence_seed_value = 0
end
FactoryBot.define do
factory :my_factory do
sequence(:label, Rails.configuration.x.factory_bot.sequence_seed_value) { |n| "Label #{n}" }
end
end
https://github.com/thoughtbot/factory_bot/blob/893eb67bbbde9d7f482852cc5133b4ab57e34b97/lib/factory_bot/sequence.rb#L13
https://github.com/thoughtbot/factory_bot/blob/893eb67bbbde9d7f482852cc5133b4ab57e34b97/spec/acceptance/sequence_spec.rb#L48
https://guides.rubyonrails.org/configuring.html#custom-configuration
sorry for my english,
I have this validation on my model
validates_uniqueness_of :price, scope: [:brand, :establishment, :presentation, :user], conditions: -> { where(created_at: Date.today.beginning_of_day..Date.today.end_of_day) }
And this is my factory and test for it.
FactoryGirl.define do
factory :price do
association :establishment
association :presentation
association :brand
association :user
price 9.99
end
end
it "is invalid on duplicated by date" do
price = create(:price)
expect(build(:price, price.attributes)).to have(1).errors
end
And I am getting...
1) Price is invalid on duplicated by date
Failure/Error: expect(build(:price, price.attributes)).to have(1).errors
expected 1 errors, got 0
Why?
validation errors get added to new objects(unsaved) only when you call valid? on the object
it "is invalid on duplicated by date" do
price = create(:price)
new_price = build(:price, price.attributes)
new_price.valid?
expect(new_price).to have(1).errors
end
I solved it, my error was on a database conception, I had a float type for price field, I changed it to decimal type to fix the error.
Afternoon All,
I've just starting learning the testing side of rails and having some trouble with this below.
Let me quickly explain what I'm trying to test. If a user creates 10 approved snippets their rank should == "Author". A snippet is created on a book which I have listed in the factories but my code is all messed up and I'm not sure how I would write this.
Here is the code I've been playing with for the testing:
describe "ranking" do
let!(:book) { Book.create }
let!(:snippet) { FactoryGirl.create(:snippet1, :book1 => book) }
context "should be author after 10 approved posts" do
10.times do
FactoryGirl.create(:snippet1)
end
user.user_rank.should == "Author"
end
end
Here are my factories:
FactoryGirl.define do
factory :admin2, class: User do
first_name "admin1"
last_name "minstrator"
password "admin1234"
profile_name "profilename"
email "admin1#admin.com"
password_confirmation "admin1234"
admin true
end
factory :user2, class: User do
first_name "user2"
last_name "man2"
password "user1234"
profile_name "profilename"
email "user2#user.com"
password_confirmation "user1234"
admin false
end
factory :book1, class: Book do
title "Book1"
approved true
size 0
end
factory :snippet1, class: Snippet do
content "Snippet1"
approved true
end
end
EDIT: Error and related code:
app/models/snippet.rb:32:in `size_limit': undefined method `size' for nil:NilClass (NoMethodError)
This relates to a validation in the model shown below:
BOOK_SIZE = {
0 => {'per' => 500, 'total' => 15000},
1 => {'per' => 700 , 'total' => 30000},
2 => {'per' => 1000, 'total' => 50000}
}
def size_limit
book_limit = self.book.size.to_i
word_count = self.content.scan(/\w+/).size.to_i
current_snippets_size = (self.book.get_word_count || 0) + word_count
errors.add(:base, "Content size is too big") unless word_count < BOOK_SIZE[book_limit]['per'] && current_snippets_size < BOOK_SIZE[book_limit]['total']
end
I think your snippet1 factory should have user_id or something like that. now you create 10 snippet without association
edit: now I read your edit. but hire is almost the same. your snippet1 factory haven't any book so error on this
self.book.size.to_i
Your let(:snippet) clause uses a :book1 attribute, but the code is checking the self.book – could that be it?
Either way, the snippet.rb excerpt you've listed has two references to .size - without knowing your line numbers, it's hard for us to tell which one is throwing the error.
So either the book attribute or the content attribute is returning nil – and so when you call size on that nil attribute you get the error that's happening.
If snippet is only valid with a book reference and a non-nil content, add validations for those conditions. If there are situations where those attributes could be nil, make sure that your code makes allowances for that.
There are (at least?) two ways to use a sequence in factory girl:
Factory.sequence :my_id do |n|
"#{n}"
end
Factory.define :my_object do |mo|
mo.id Factory.next :my_id
end
and simply doing it inline:
Factory.define :my_object do |mo|
mo.sequence(:id) { |n| "#{n}" }
end
My question is this. If I use the inline version in two different factories, will there be two different sequences that both start at 1 and increment in tandem...meaning that if I create one of each type of factory object they will both have id 1?
If I use the externally defined sequence in two different factories am I guaranteed to get unique ids across the two objects? Meaning will the ids of each object be different?
I am trying to confirm if the behavior above is accurate because I'm working with a completely goofy data model trying to get rspec & factory girl to play nice with it. The designer of the database set things up so that different objects have to have ids generated that are unique across a set of unrelated objects. Changing the data model at this point is not a feasible solution though I'd really love to drag this stuff back onto the Rails.
When using externally defined sequences in two different factories you will see incrementing ids across the factories. However, when using inline sequences each factory will have their own sequence.
I created the example rake task below to illustrate this. It displays the following results:
*** External FactoryGirl Sequence Test Results ***
User Name: Name 1
User Name: Name 2
User Name: Name 3
User Name: Name 4
Role: Name 5
Role: Name 6
Role: Name 7
Role: Name 8
*** Internal FactoryGirl Sequence Test Results ***
User Name: Name 1
User Name: Name 2
User Name: Name 3
User Name: Name 4
Role: Role 1
Role: Role 2
Role: Role 3
Role: Role 4
As you can see, when using external sequences the number continues to increase as you move from the user to the role. However when using an inline sequence the increments are independent of each other.
The following schema files were used for this example:
create_table "users", :force => true do |t|
t.string "name"
t.string "email"
end
create_table "roles", :force => true do |t|
t.string "name"
end
The example rake task is:
require 'factory_girl_rails'
namespace :sequencetests do
Rake::Task[:environment].invoke
task :external do
FactoryGirl.factories.clear
desc "Factory Girl Sequence Test using an externally defined sequence"
puts "*** External FactoryGirl Sequence Test Results ***"
FactoryGirl.define do
sequence :name do |n|
"Name #{n}"
end
factory :user do |u|
name
end
factory :role do |r|
name
end
end
users = buildit(:user)
roles = buildit(:role)
puts( showit(users, "User Name: "))
puts( showit(roles, "Role: "))
end
task :inline do
FactoryGirl.factories.clear
puts "*** Internal FactoryGirl Sequence Test Results ***"
desc "Factory Girl Sequence Test using an inline sequence"
FactoryGirl.define do
factory :user do |u|
u.sequence(:name) {|n| "Name #{n}" }
end
factory :role do |r|
r.sequence(:name) {|n| "Role #{n}" }
end
end
users = buildit(:user)
roles = buildit(:role)
puts( showit(users, "User Name: "))
puts( showit(roles, "Role: "))
end
end
task sequencetests: ['sequencetests:external', 'sequencetests:inline']
def buildit(what)
items = []
4.times do
items << FactoryGirl.build(what)
end
items
end
def showit(items, prefix = "Name: ")
results = ""
items.each do |item|
results += "#{prefix}#{item.name}\n"
end
results
end
I hope this helps explain the different possibilities when using sequences in FactoryGirl.
Yes, the inline versions will create 2 independent sequences, each starting at 1
I have a Person model that has a many-to-many relationship with an Email model and I want to create a factory that lets me generate a first and last name for the person (this is already done) and create an email address that is based off of that person's name. Here is what I have for create a person's name:
Factory.sequence :first_name do |n|
first_name = %w[FirstName1 FirstName2] # ... etc (I'm using a real subset of first names)
first_name[(rand * first_name.length)]
end
Factory.sequence :last_name do |n|
last_name = %w[LastName1 LastName2] # ... etc (I'm using a real subset of last names)
last_name[(rand * last_name.length)]
end
Factory.define :person do |p|
#p.id ???
p.first_name { Factory.next(:first_name) }
p.last_name { Factory.next(:last_name) }
#ok here is where I'm stuck
#p.email_addresses {|p| Factory(:email_address_person_link) }
end
Factory.define :email_address_person_link do |eapl|
# how can I link this with :person and :email_address ?
# eapl.person_id ???
# eapl.email_address_id ???
end
Factory.define :email_address do |e|
#how can I pass p.first_name and p.last_name into here?
#e.id ???
e.email first_name + "." + last_name + "#test.com"
end
Ok, I think I understand what you're asking now. Something like this should work (untested, but I've done something similar in another project):
Factory.define :person do |f|
f.first_name 'John'
f.last_name 'Doe'
end
Factory.define :email do |f|
end
# This is optional for isolating association testing; if you want this
# everywhere, add the +after_build+ block to the :person factory definition
Factory.define :person_with_email, :parent => :person do |f|
f.after_build do |p|
p.emails << Factory(:email, :email => "#{p.first_name}.#{p.last_name}#gmail.com")
# OR
# Factory(:email, :person => p, :email => "#{p.first_name}.#{p.last_name}#gmail.com")
end
end
As noted, using a third, separate factory is optional. In my case I didn't always want to generate the association for every test, so I made a separate factory that I only used in a few specific tests.
Use a callback (see FG docs for more info). Callbacks get passed the current model being built.
Factory.define :person do |p|
p.first_name { Factory.next(:first_name) }
p.last_name { Factory.next(:last_name) }
p.after_build { |m| p.email_addresses << "#{m.first_name}.#{m.last_name}#test.com" }
end
I think that works.
You could also save yourself some work by looking into using the Faker gem which creates realistic first and last names and e-mail addresses for you.