Factory Girl: using sequence inline vs not inline - ruby-on-rails

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

Related

Testing association validations with RSpec and FactoryGirl

I'm currently trying to write an RSpec test for a validation method. This method is triggered when the record is updated, saved or created. Here is what I have so far:
product.rb (model)
class Product < ActiveRecord::Base
validate :single_product
# Detects if a product has more than one SKU when attempting to set the single product field as true
# The sku association needs to map an attribute block in order to count the number of records successfully
# The standard self.skus.count is performed using the record ID, which none of the SKUs currently have
#
# #return [boolean]
def single_product
if self.single && self.skus.map { |s| s.active }.count > 1
errors.add(:single, " product cannot be set if the product has more than one SKU.")
return false
end
end
end
products.rb (FactoryGirl test data)
FactoryGirl.define do
factory :product do
sequence(:name) { |n| "#{Faker::Lorem.word}#{Faker::Lorem.characters(8)}#{n}" }
meta_description { Faker::Lorem.characters(10) }
short_description { Faker::Lorem.characters(15) }
description { Faker::Lorem.characters(20) }
sku { Faker::Lorem.characters(5) }
sequence(:part_number) { |n| "GA#{n}" }
featured false
active false
sequence(:weighting) { |n| n }
single false
association :category
factory :product_skus do
after(:build) do |product, evaluator|
build_list(:sku, 3, product: product)
end
end
end
end
product_spec.rb (unit test)
require 'spec_helper'
describe Product do
describe "Setting a product as a single product" do
let!(:product) { build(:product_skus, single: true) }
context "when the product has more than one SKU" do
it "should raise an error" do
expect(product).to have(1).errors_on(:single)
end
end
end
end
As you can see from the singe_product method, I'm trying to trigger an error on the single attribute when the single attribute is set to true and the product has more than one associated SKU. However, when running the test the product has no associated SKUs and therefore fails the unit test shown above.
How do I build a record and generate associated SKUs which can be counted (e.g: product.skus.count) and validated before they are all created in FactoryGirl?
You could write this like
it 'should raise an error' do
product = build(:product_skus, single: true)
expect(product).not_to be_valid
end

Rspec Create multiple posts to check ranking

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.

Use a factory's sequence to generate unique phone numbers

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

Testing an expected order of an array in RSpec / Rails

In a RSpec spec file I have the following test
it 'should return 5 players with ratings closest to the current_users rating' do
matched_players = User.find(:all,
:select => ["*,(abs(rating - current_user.rating)) as player_rating"],
:order => "player_rating",
:limit => 5)
# test that matched_players array returns what it is suppose to
end
How would I complete this to test that matched_players is returning the correct users.
I think you should first introduce some test users to the test DB (using for example a Factory) and afterwards see that the test is returning the correct ones.
Also it would make more sense to have a method in your model that would return the matched users.
For example:
describe "Player matching" do
before(:each) do
#user1 = FactoryGirl.create(:user, :rating => 5)
...
#user7 = FactoryGirl.create(:user, :rating => 3)
end
it 'should return 5 players with ratings closest to the current_users rating' do
matched_players = User.matched_players
matched_players.should eql [#user1,#user3,#user4,#user5,#user6]
end
end
Your model shouldn't know about your current user (the controllers know about this concept)
You need to extract this as a method on the User class otherwise there's no point in testing it, i.e. why test logic that isn't even in your app code?
The function that gets the matched players doesn't need to know about the current user, or any user for that matter, just the rating.
To test it, create a bunch of User instances, call the method, and see that the result is a list of the correct user instances you expect.
models/user.rb
class User < ActiveRecord::Base
...
def self.matched_players(current_user_rating)
find(:all,
select: ["*,(abs(rating - #{current_user_rating)) as match_strength"],
order: "match_strength",
limit: 5)
end
...
end
spec/models/user_spec.rb
describe User do
...
describe "::matched_players" do
context "when there are at least 5 users" do
before do
10.times.each do |n|
instance_variable_set "#user#{n}", User.create(rating: n)
end
end
it "returns 5 users whose ratings are closest to the given rating, ordered by closeness" do
matched_players = described_class.matched_players(4.2)
matched_players.should == [#user4, #user5, #user3, #user6, #user2]
end
context "when multiple players have ratings close to the given rating and are equidistant" do
# we don't care how 'ties' are broken
it "returns 5 users whose ratings are closest to the given rating, ordered by closeness" do
matched_players = described_class.matched_players(4)
matched_players[0].should == #user4
matched_players[1,2].should =~ [#user5, #user3]
matched_players[3,4].should =~ [#user6, #user2]
end
end
end
context "when there are fewer than 5 players in total" do
...
end
...
end
...
end

How to create a ruby enum and parse the value from querystring?

I need to create an enumeration that I will need to initialize from a value from the querystring.
Example of what I have and what I need to do:
class UserType
NONE = 0
MEMBER = 1
ADMIN = 2
SUPER = 3
end
Now in my querystring I will have:
/users/load_by_type?type=2
Now in my controller I will get the value 2 from the querystring, I then need to have a UserType object which has the value 'MEMBER'.
How can I do this?
If my class isn't really a good enumeration hack, please advise.
How about something like this.
require 'active_record'
# set up db
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
# define schema
ActiveRecord::Schema.define do
suppress_messages do
create_table :users do |t|
t.string :name
t.string :role
end
end
end
# define class
class User < ActiveRecord::Base
Roles = %w[none member admin super].map(&:freeze)
validates_inclusion_of :role, in: Roles
end
# specification
describe User do
before { User.delete_all }
let(:valid_role) { User::Roles.first }
let(:invalid_role) { valid_role.reverse }
it 'is valid if its role is in the Roles list' do
User.new.should_not be_valid
User.new(role: valid_role).should be_valid
User.new(role: invalid_role).should_not be_valid
end
let(:role) { User::Roles.first }
let(:other_role) { User::Roles.last }
it 'can find users by role' do
user_with_role = User.create! role: role
user_with_other_role = User.create! role: other_role
User.find_all_by_role(role).should == [user_with_role]
end
end
It does have the disadvantage of using an entire string (255 chars) for the enumeration method, but it also has the advantage of readability and ease of use (it would probably come in as "/users/load_by_role?role=admin"). Besides, if at some point it winds up costing too much, it should be easy to update to use a small integer.
I think I'd rather use hashes for this kind of thing, but just for fun:
class Foo
BAR = 1
STAN = 2
class << self
def [](digit)
constants.find { |const| const_get(const) == digit }
end
end
end
puts Foo[1] # BAR
puts Foo[2] # STAN

Resources