Testing has_many association with RSpec - ruby-on-rails

I'm trying to test the Hour model with RSpec, namely the class method 'find_days_with_no_hours' that behaves like a scope. Business has_many Hours associated through STI.
find_days_with_no_hours needs to be called through a Business object and I can't figure out how to set this up in the RSpec test.
I want to be able to test something like:
bh = #business.hours.find_days_with_no_hours
bh.length.should == 2
I've tried various approaches, like creating a Business object (with, say, Business.create), then setting #business.hours << mock_model(BusinessHour, ..., ..., ...) but that doesn't work.
How is this normally done?
class Business < ActiveRecord::Base
has_many :hours, :as => :hourable
end
class Hour < ActiveRecord::Base
belongs_to :hourable, :polymorphic => true
def self.find_days_with_no_hours
where("start_time IS NULL")
end
end

You can't test an arel method by creating the object via mocks. Arel is going to go straight into the database, and not see any mocks or anything that you've created in memory. I would grab factory_girl and then define an hour factory for yourself:
Factory.define :hour do |f|
f.start_time {Time.now}
end
Factory.define :unstarted_day, :parent => :hour do |f|
f.start_time nil
end
And then in your test...
business = Factory.create(:business)
business.hours << Factory.create(:unstarted_day)
bh = business.hours.find_days_with_no_hours
bh.length.should == 1
However, factory_girl is just a personal preference for setting up known state, you can just as easily use create statements or fixtures, the problem for you was trying to use mock_model() (which prevents a database hit), and then using a method that queries the database.

Related

Using a method within model, calling it from view

I have an Update model which belongs to users.
To show all of one user's friends' Updates, I am doing something like:
Update.where("user_id" => [array_of_friend_ids])
I know the "right" way of doing things is to create a method to create the above array. I started writing the method but it's only half-working. Currently I have this in my user model:
def self.findfriends(id)
#friendarray = []
#registered_friends = Friend.where("user_id" => id)
#registered_friends.each do |x|
#friendarray << x.friend_id
end
return #friendarray
end
I am doing the entire action in the view with:
<% #friendinsert = User.findfriends(current_user.id) %>
<% #friendarray = [] %>
<% #friendarray << #friendinsert %>
<%= #friendarray.flatten! %>
Then I'm calling Update.where("user_id" => #friendarray) which works. But obviously I'm doing things in a very hacky way here. I'm a bit confused as to when Rails can "see" certain variables from models and methods in the view. What's the best way to go about inserting an array of IDs to find their Updates, since I'm not supposed to use much logic in the view itself?
Mattharick is right about using associations. You should use associations for the question you mentioned in description of your question. If we come to the question at the title of your question;
let's say you have a User model.
These two methods are different:
def self.testing
puts "I'm testing"
end
and the other one is:
def testing
puts "I'm testing"
end
Pay attention to the self keyword. self keyword makes method a Class method. Which you can call it from your controllers or views like: User.testing.
But the one with out testing is a instance method. Which can be called like:
u = User.last
u.testing
Second one gives you possibility to use attributes of the 'instance' inside your model.
For example, you can show name of your instance in that method just like this?
def testing
puts "Look, I'm showing this instance's name which is: #{name}"
end
These are powerful stuff.
Practise on them.
Simple add another association to your project.
class User < ActiveRecord::Base
has_many :friendship
has_many :friends, :through => :friendship, :class_name => User, :foreign_key => :friend_id
has_many :friendship
has_many :users, :through => :friendship
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => User
end
I don't know if my synrax is correct, please try out.
Friendship has the attributes user_id and friend_id.
After that you should be able to do something like following to get the updates of a friend:
User.last.friends.last.updates
You can work with normal active record queries instead of hacky arrays..

Proper way to do this with ActiveRecord?

Say I have two classes,
Image and Credit
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
end
I want associate a Credit when Image is created, acting a bit like a tag. Essentially, I want behavior like Credit.find_or_create_by_name, but in the client code using Credit, it would be much cleaner if it was just a Create. I can't seem to figure out a way to bake this into the model.
Try this:
class Image < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
after_create { Credit.associate_object(self) }
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
def self.associate_object(object, association='images')
credit = self.find_or_create_by_name(object.credit_name)
credit.send(association) << object
credit.save
end
end
Then when you create an image what you can do is something like
Image.create(:attr1 => 'value1', :attr2 => 'value2', ..., :credit_name => 'some_name')
And it will take the name that you feed into the :credit_name value and use it in the after_create callback.
Note that if you decided to have a different object associated with Credit later on (let's say a class called Text), you could do still use this method like so:
class Text < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
before_create { Credit.associate_object(self, 'texts') }
end
Although at that point you probably would want to consider making a SuperClass for all of the classes that belong_to credit, and just having the superclass handle the association. You might also want to look at polymorphic relationships.
This is probably more trouble than it's worth, and is dangerous because it involves overriding the Credit class's initialize method, but I think this might work. My advice to you would be to try the solution I suggested before and ditch those gems or modify them so they can use your method. That being said, here goes nothing:
First you need a way to get at the method caller for the Credit initializer. Let's use a class I found on the web called CallChain, but we'll modify it for our purposes. You would probably want to put this in your lib folder.
class CallChain
require 'active_support'
def self.caller_class
caller_file.split('/').last.chomp('.rb').classify.constantize
end
def self.caller_file(depth=1)
parse_caller(caller(depth+1).first).first
end
private
#Stolen from ActionMailer, where this was used but was not made reusable
def self.parse_caller(at)
if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
file = Regexp.last_match[1]
line = Regexp.last_match[2].to_i
method = Regexp.last_match[3]
[file, line, method]
end
end
end
Now we need to overwrite the Credit classes initializer because when you make a call to Credit.new or Credit.create from another class (in this case your Image class), it is calling the initializer from that class. You also need to ensure that when you make a call to Credit.create or Credit.new that you feed in :caller_class_id => self.id to the attributes argument since we can't get at it from the initializer.
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
attr_accessor :caller_class_id
def initialize(args = {})
super
# only screw around with this stuff if the caller_class_id has been set
if caller_class_id
caller_class = CallChain.caller_class
self.send(caller_class.to_param.tableize) << caller_class.find(caller_class_id)
end
end
end
Now that we have that setup, we can make a simple method in our Image class which will create a new Credit and setup the association properly like so:
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
# for building
def build_credit
Credit.new(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
# for creating
# if you wanted to have this happen automatically you could make the method get called by an 'after_create' callback on this class.
def create_credit
Credit.create(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
end
Again, I really wouldn't recommend this, but I wanted to see if it was possible. Give it a try if you don't mind overriding the initialize method on Credit, I believe it's a solution that fits all your criteria.

Is validates_presence_of the preferred technique to require a has_many relationship

Basically: My model requires at least one instance of an associated model be present. Should I use validates_presence_of to assert this validation, or should I write some custom validation code?
Here are the essentials of my model:
class Event < ActiveRecord::Base
has_and_belongs_to_many :channels
validates_presence_of :channels, :message => "can't be empty"
end
(I assume things would be the same if I used has_many in place of has_and_belongs_to_many.)
Instead of the validates_presence_of line I could do this:
def validate
errors.add(:channels, "can't be empty") if channels.size < 1
end
I replaced the latter with the former in the Rails app I'm working on and am wondering if there might be any problems.
So to be more sure, I wrote the following rspec coverage, and both implementations respond the same:
describe Event do
before do
#net = Factory.create(:network)
#net_config = Factory.create(:network_config, :network => #net)
end
it "must have a channel" do
e = Factory.build(:event, :network => #net, :channels => [])
e.should have(1).error_on(:channels)
end
end
That is, if I remove the validation code, the above spec fails; if I put in either version of the validation code, the above spec passes.
So I might assume that my new implementation is ok. But I've read that validates_presence triggers a database load which, in turn, would wipe out any in-memory objects constructed from nested attributes. The proxy_target method, on the other hand, will return the in-memory objects without triggering a load. Some links on proxy_target: http://rubydoc.info/docs/rails/ActiveRecord/Associations/AssociationProxy http://withoutscope.com/2008/8/22/don-t-use-proxy_target-in-ar-association-extensions
In my particular case I'm not using ActiveRecord::Relation, but I wonder if I need to be cautious about this.

testing a has_one relationship?

I have an object
class User < ActiveRecord::Base
has_one :subscription
end
and I have this test:
it "should increment shipped count when item_shipped" do
#user.attributes = #valid_attributes
#user.save
subscription = mock_model(Subscription)
subscription.stub!(:item_shipped!)
subscription.stub!(:user_id)
#user.subscription = subscription
lambda{#user.item_shipped!}.should change{#user.shipped_count}.by(1)
end
But I am getting an error:
1)
Spec::Mocks::MockExpectationError in 'User should increment shipped count when item_shipped'
Mock "Subscription_1113" received unexpected message :[]= with ("user_id", 922717357)
./spec/models/user_spec.rb:29:
I am not sure how to mock this out and I can't seem to find any references to this kind of thing.
Instead of mocking Subscription, try stubbing out the methods on an actual Subscription instead:
subscription = Subscription.new
subscription.stub!(:item_shipped!)
subscription.stub!(:user_id)
#user.subscription = subscription
Mocks can be brittle. Any call to a mock must be anticipated and declared as an expectation. It doesn't appear that this particular test needs that model mocked in any case.
EDIT: Also remember to declare any return values that the calling class depends on. In your case this might look like:
subscription.stub!(:item_shipped!).and_return(true)
subscription.stub!(:user_id).and_return(#user.id)
etc.
Again, if you're not asserting that a method on your mocked model should be called, then the only thing mocking does here is make your test brittle. Mocks are meant for things like:
subscription.should_receive(:some_method).once
Otherwise you simply need to stub out methods that have undesirable side effects that don't concern your spec.
Setting up associations for tests is made easier with factories: (untested)
Factory.define :subscriber, :class => User do |f|
f.name "Moe Howard"
f.association :subscription, :factory => :subscription
end
Factory.define :subscription, :class => Subscription do |f|
end
it "should increment shipped count when item_shipped" do
#user = Factory.create(:subscriber)
lambda{#user.item_shipped!}.should change{#user.shipped_count}.by(1)
end
Of course you're not really testing the association here -- you're testing the item_shipped method, which is what you really wanted.
change: mock_model(Subscription) to mock_model(Subscription).as_null_object
which will allow for any messages to be sent to the object (assuming this is an acceptable behavior in your case)

How can I improve this Rails code?

I'm writing a little browser game as a project to learn RoR with and I'm quite new to it.
This is a little method that's called regularly by a cronjob.
I'm guessing there should be some way of adding elements to the potions array and then doing a bulk save at the end, I'm also not liking hitting the db each time in the loop to get the number of items for the market again.
def self.restock_energy_potions
market = find_or_create_market
potions = EnergyPotion.find_all_by_user_id(market.id)
while (potions.size < 5)
potion = EnergyPotion.new(:user_id => market.id)
potion.save
potions = EnergyPotion.find_all_by_user_id(market.id)
end
end
I'm not sure I'm understanding your question. Are you looking for something like this?
def self.restock_energy_potions
market = find_or_create_market
potions = EnergyPotion.find_all_by_user_id(market.id)
(potions.size...5).each {EnergyPotion.new(:user_id => market.id).save }
end
end
Note the triple dots in the range; you don't want to create a potion if there are already 5.
Also, if your potions were linked (e.g. by has_many) you could create them through the market.potions property (I'm guessing here, about the relationship between users and markets--details depend on how your models are set up) and save them all at once. I don't think the data base savings would be significant though.
Assuming that your market/user has_many potions, you can do this:
def self.restock_energy_potions
market = find_or_create_market
(market.potions.size..5).each {market.potions.create(:user_id => market.id)}
end
a) use associations:
class Market < AR::Base
# * note that if you are not dealing with a legacy schema, you should
# rename user_id to market_id and remove the foreigh_key assignment.
# * dependent => :destroy is important or you'll have orphaned records
# in your database if you ever decide to delete some market
has_many :energy_potions, :foreign_key => :user_id, :dependent => :destroy
end
class EnergyPotion < AR::Base
belongs_to :market, :foreign_key => :user_id
end
b) no need to reload the association after adding each one. also move the functionality
into the model:
find_or_create_market.restock
class Market
def restock
# * note 4, not 5 here. it starts with 0
(market.energy_potions.size..4).each {market.energy_potions.create!}
end
end
c) also note create! and not create.
you should detect errors.
error handling depends on the application.
in your case since you run it from cron you can do few things
* send email with alert
* catch exceptions and log them, (exception_notifier plugin, or hoptoad hosted service)
* print to stderror and configuring cron to send errors to some email.
def self.restock_potions
market = find_or_create
market.restock
rescue ActiveRecord::RecordInvalid
...
rescue
...
end

Resources