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.
Related
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
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
I have a Code model factory like this:
Factory.define :code do |f|
f.value "code"
f.association :code_type
f.association(:codeable, :factory => :portfolio)
end
But when I test my controller with a simple test_should_create_code like this:
test "should create code" do
assert_difference('Code.count') do
post :create, :code => Factory.attributes_for(:code)
end
assert_redirected_to code_path(assigns(:code))
end
... the test fails. The new record is not created.
In the console, it seems that attributes_for does not return all required attributes like the create does.
rob#compy:~/dev/my_rails_app$ rails console test
Loading test environment (Rails 3.0.3)
irb(main):001:0> Factory.create(:code)
=> #<Code id: 1, code_type_id: 1, value: "code", codeable_id: 1, codeable_type: "Portfolio", created_at: "2011-02-24 10:42:20", updated_at: "2011-02-24 10:42:20">
irb(main):002:0> Factory.attributes_for(:code)
=> {:value=>"code"}
Any ideas?
Thanks,
You can try something like this:
(Factory.build :code).attributes.symbolize_keys
Check this: http://groups.google.com/group/factory_girl/browse_thread/thread/a95071d66d97987e)
This one doesn't return timestamps etc., only attributes that are accessible for mass assignment:
(FactoryGirl.build :position).attributes.symbolize_keys.reject { |key, value| !Position.attr_accessible[:default].collect { |attribute| attribute.to_sym }.include?(key) }
Still, it's quite ugly. I think FactoryGirl should provide something like this out of the box.
I opened a request for this here.
I'd suggest yet an other approach, which I think is clearer:
attr = attributes_for(:code).merge(code_type: create(:code_type))
heres what I end up doing...
conf = FactoryGirl.build(:conference)
post :create, {:conference => conf.attributes.slice(*conf.class.accessible_attributes) }
I've synthesized what others have said, in case it helps anyone else. To be consistent with the version of FactoryGirl in question, I've used Factory.build() instead of FactoryGirl.build(). Update as necessary.
def build_attributes_for(*args)
build_object = Factory.build(*args)
build_object.attributes.slice(*build_object.class.accessible_attributes).symbolize_keys
end
Simply call this method in place of Factory.attributes_for:
post :create, :code => build_attributes_for(:code)
The full gist (within a helper module) is here: https://gist.github.com/jlberglund/5207078
In my APP/spec/controllers/pages_controllers_spec.rb I set:
let(:valid_attributes) { FactoryGirl.attributes_for(:page).merge(subject: FactoryGirl.create(:theme), user: FactoryGirl.create(:user)) }
Because I have two models associated. This works too:
FactoryGirl.define do
factory :page do
title { Faker::Lorem.characters 12 }
body { Faker::Lorem.characters 38 }
discution false
published true
tags "linux, education, elearning"
section { FactoryGirl.create(:section) }
user { FactoryGirl.create(:user) }
end
end
Here's another way. You probably want to omit the id, created_at and updated_at attributes.
FactoryGirl.build(:car).attributes.except('id', 'created_at', 'updated_at').symbolize_keys
Limitations:
It does not generate attributes for HMT and HABTM associations (as these associations are stored in a join table, not an actual attribute).
Association strategy in the factory must be create, as in association :user, strategy: :create. This strategy can make your factory very slow if you don't use it wisely.
I'm fairly new to rails, working on a Rails 3 app with a Profile model for users.
In the profile Model I'd like to have a "name" entry, and I'd like to be able to access logical variations of it using simple syntax like:
user.profile.name = "John Doe"
user.profile.name.first = "John"
user.profile.name.last = "Doe"
Is this possible, or do I need to stick with "first_name" and "last_name" as my fields in this model?
It's possible, but I wouldn't recommend it.
I would just stick with first_name and last_name if I were you and add a method fullname:
def fullname
"#{first_name} #{last_name}"
end
Edit:
If you really do want user.profile.name, you could create a Name model like this:
class Name < ActiveRecord::Base
belongs_to :profile
def to_s
"#{first} #{last}"
end
end
This allows you to do:
user.profile.name.to_s # John Doe
user.profile.name.first # John
user.profile.name.last # Doe
The other answers are all correct, in so far as they ignore the #composed_of aggregator:
class Name
attr_reader :first, :last
def initialize(first_name, last_name)
#first, #last = first_name, last_name
end
def full_name
[#first, #last].reject(&:blank?).join(" ")
end
def to_s
full_name
end
end
class Profile < ActiveRecord::Base
composed_of :name, :mapping => %w(first_name last_name)
end
# Rails console prompt
> profile = Profile.new(:name => Name.new("Francois", "Beausoleil"))
> profile.save!
> profile = Profile.find_by_first_name("Francois")
> profile.name.first
"Francois"
As noted on the #composed_of page, you must assign a new instance of the aggregator: you cannot just replace values within the aggregator. The aggregator class acts as a Value, just like a simple string or number.
I also sent a response yesterday with a very similar answer: How best to associate an Address to multiple models in rails?
As Capt. Tokyo said that's a horrible idea but here's how you would do it:
rails g model User full_name:hash
Then you would store data in it like so:
user = User.new
user.full_name = {:first => "Forrest", :last => "Gump"}
Now your problems begin.
To search the field requires both names and you can't do a partial search like searching for all people with the same last name. Worst of all you can store anything in the field! So imagine another programmer mistypes one of the field names so for a week you have {:fist => "Name", :last => "Last"} being inserted into the database! Noooooooooooooooooo!
If you used proper field names you could do this:
user = User.new(:first_name => "First", :last_name => "Last")
Easy to read and no need for hashes. Now that you know how to do it the wrong way, do it the right way. :)
FYI (assume you have a field fullname. ie your profile.name = "John Doe")
class Profile
def name
#splited_name ||= fullname.split # #splited_name would cache the result so that no need to split the fullname every time
end
end
Now, you could do something like this:
user.profile.fullname # "John Doe"
user.profile.name.first # "John"
user.profile.name.last # "Doe"
Note the following case:
user.profile.fullname = "John Ronald Doe"
user.profile.name.first # "John"
user.profile.name.second # "Ronald"
user.profile.name.last # "Doe"
I agree with captaintokyo. You won't miss out the middle names.
Also this method assume no Chinese, Japanese names are input. It's because those names contain no spaces in between first name and last name normally.
I have a user model in which I have a method for seeing if the user has earned a "badge"
def check_if_badges_earned(user)
if user.recipes.count > 10
award_badge(1)
end
If they have earned a badge, the the award_badge method runs and gives the user the associated badge. Can I do something like this?
def check_if_badges_earned(user)
if user.recipes.count > 10
flash.now[:notice] = "you got a badge!"
award_badge(1)
end
Bonus Question! (lame, I know)
Where would the best place for me to keep all of these "conditions" for which my users could earn badges, similar to stackoverflows badges I suppose. I mean in terms of architecture, I already have badge and badgings models.
How can I organize the conditions in which they are earned? some of them are vary complex, like the user has logged in 100 times without commenting once. etc. so there doesn’t seem to be a simple place to put this sort of logic since it spans pretty much every model.
I'm sorry for you but the flash hash is not accessible in models, it gets created when the request is handled in your controller. You still can use implement your method storing the badge infos (flash message included) in a badge object that belongs to your users:
class Badge
# columns:
# t.string :name
# seed datas:
# Badge.create(:name => "Recipeador", :description => "Posted 10 recipes")
# Badge.create(:name => "Answering Machine", :description => "Answered 1k questions")
end
class User
#...
has_many :badges
def earn_badges
awards = []
awards << earn(Badge.find(:conditions => { :name => "Recipeador" })) if user.recipes.count > 10
awards << earn(Badge.find(:conditions => { :name => "Answering Machine" })) if user.answers.valids.count > 1000 # an example
# I would also change the finds with some id (constant) for speedup
awards
end
end
then:
class YourController
def your_action
#user = User.find(# the way you like)...
flash[:notice] = "You earned these badges: "+ #user.earn_badges.map(:&name).join(", ")
#...
end
end