testing with rspec codeschool level 5 challenge 4 - ruby-on-rails

Here is the base question for the test:
We've changed the code below so we're mocking ZombieMailer.tweet method instead of the entire method. There is still more to do to make this example work. First, finish the let(:mail) statement below by creating a stub with a deliver method that returns true.
Then update the mock so the call to the tweet method returns the mail stub you created.
Unfortunately I can not alter the models, question, or mailer.
Models:
# tweet.rb
class Tweet < ActiveRecord::Base
belongs_to :zombie
validates :message, presence: true
attr_accessible :message
after_create :email_tweeter
def email_tweeter
ZombieMailer.tweet(zombie, self).deliver
end
private :email_tweeter
end
# zombie.rb
class Zombie < ActiveRecord::Base
has_many :tweets
validates :email, presence: true
attr_accessible :email
end
Mailer:
class ZombieMailer < ActionMailer::Base
def tweet(zombie, tweet)
mail(:from => 'admin#codeschool.com',
:to => zombie.email,
:subject => tweet.message)
end
end
I keep bouncing around on this and could use a few pointers. Here is what I have been working with now that I got past test question 3. Now they are asking to add the deliver method, but his method does not exist, or at least I am not placing it in the correct place.
UPDATED
describe Tweet do
context 'after create' do
let(:zombie) { Zombie.create(email: 'anything#example.org') }
let(:tweet) { zombie.tweets.new(message: 'Arrrrgggghhhh') }
let(:mail) { stub(:deliver => true) }
it 'calls "tweet" on the ZombieMailer' do
ZombieMailer.should_receive(:tweet).returns(:mail)
tweet.save
end
end
end
And the error message is:
Failures:
1) Tweet after create calls "tweet" on the ZombieMailer
Failure/Error: ZombieMailer.should_receive(:tweet).returns(:mail)
NoMethodError:
undefined method `returns' for #<RSpec::Mocks::MessageExpectation:0x0000000519ab90>
# zombie_spec.rb:8:in `block (3 levels) '
Finished in 0.37725 seconds
1 example, 1 failure
Failed examples:
rspec zombie_spec.rb:7 # Tweet after create calls "tweet" on the ZombieMailer
Any rspec peeps out there can point me in the right direction as to what I am missing here? Thank you.

Thank you to #Paritosh and #Alex from the comments above, here is the final answer.
describe Tweet do
context 'after create' do
let(:zombie) { Zombie.create(email: 'anything#example.org') }
let(:tweet) { zombie.tweets.new(message: 'Arrrrgggghhhh') }
let(:mail) { stub(:deliver => true) }
it 'calls "tweet" on the ZombieMailer' do
ZombieMailer.should_receive(:tweet).and_return(mail)
tweet.save
end
end
end

Related

Automatically emailing a user when they create a post

I am a trainee full stack developer learning ruby on rails and am in month 1 of a 6 month intensive course.
I am working on a 'reddit' style app where users can create topics, posts and comments.
I am trying to automatically email a user when they create a new post.
I am using ActionMailer for this.
I am working on an after_create callback in my post model and a method in a mailer called 'favorite_mailer'.
The problem I am facing, is that I am unable to successfully implement an after_create callback, which triggers an email to be automatically sent to a user after they create a post.
I have defined a method in my mailer called new_post, which should receive 2 arguments (user, post).
I have defined a callback method in my Post model called send_new_post email but can't make it pass my Rspec tests.
Any help will be greatly appreciated.
I have created the following Post model spec:
describe "send_new_post_email" do
it "triggers an after_create callback called send_new_post_email" do
expect(post).to receive(:send_new_post_email).at_least(:once)
post.send(:send_new_post_email)
end
it "sends an email to users when they create a new post" do
expect(FavoriteMailer).to receive(:new_post).with(user, post).and_return(double(deliver_now: true))
post.save
end
end
Here is my Post Model (the relevant bit being the send_new_post_email callback):
class Post < ActiveRecord::Base
belongs_to :topic
belongs_to :user
has_many :comments, dependent: :destroy
has_many :votes, dependent: :destroy
has_many :favorites, dependent: :destroy
has_many :labelings, as: :labelable
has_many :labels, through: :labelings
after_create :create_vote
after_create :create_favorite
after_create :send_new_post_email
default_scope { order('rank DESC') }
validates :title, length: { minimum: 5 }, presence: true
validates :body, length: { minimum: 20 }, presence: true
validates :topic, presence: true
validates :user, presence: true
def up_votes
votes.where(value: 1).count
end
def down_votes
votes.where(value: -1).count
end
def points
votes.sum(:value)
end
def update_rank
age_in_days = (created_at - Time.new(1970,1,1)) / 1.day.seconds
new_rank = points + age_in_days
update_attribute(:rank, new_rank)
end
private
def create_vote
user.votes.create(value: 1, post: self)
end
def create_favorite
user.favorites.create(post: self)
end
def send_new_post_email
FavoriteMailer.new_post(self.user, self)
end
end
Finally, here is my mailer:
class FavoriteMailer < ApplicationMailer
default from: "charlietarr1#gmail.com"
def new_comment(user, post, comment)
# #18
headers["Message-ID"] = "<comments/#{comment.id}#your-app-name.example>"
headers["In-Reply-To"] = "<post/#{post.id}#your-app-name.example>"
headers["References"] = "<post/#{post.id}#your-app-name.example>"
#user = user
#post = post
#comment = comment
# #19
mail(to: user.email, subject: "New comment on #{post.title}")
end
def new_post(user, post)
# #18
headers["Message-ID"] = "<post/#{post.id}#your-app-name.example>"
headers["In-Reply-To"] = "<post/#{post.id}#your-app-name.example>"
headers["References"] = "<post/#{post.id}#your-app-name.example>"
#user = user
#post = post
# #19
mail(to: user.email, subject: "You have favorited #{post.title}")
end
end
I would not use a model callback to handle these kind of lifecycle events.
Why?
Because it is going to be called whenever you create a record which means you will have to override it in your tests.
ActiveRecord models can easily become godlike and bloated and notifying users is a bit beyond the models job of maintaining data and business logic.
It will also get in the way of delegating the notifications to a background job, which is very important if you need to send multiple emails.
So what then?
Well, we could stuff it in the controller. But that might not be optimal since controllers are PITA to test and we like 'em skinny.
So let's create an object with the single task of notifying the user:
module PostCreationNotifier
def self.call(post)
FavoriteMailer.new_post(post.user, post)
end
end
And then we add it to the controller action:
def create
#post = Post.new(post_params)
if #post.save
redirect_to #post
PostCreationNotifier.call(#post)
else
render :new
end
end
But - this probably is not what you want! Doh! It will only notify the creator that she just created a post - and she already knows that!
If we want to notify all the participants of a response we probably need to look at the thread and send an email to all of the participants:
module PostCreationNotifier
def self.call(post)
post.thread.followers.map do |f|
FavoriteMailer.new_post(f, post)
end
end
end
describe PostCreationNotifier do
let(:followers) { 2.times.map { create(:user) } }
let(:post){ create(:post, thread: create(:thread, followers: followers)) }
let(:mails) { PostCreationNotifier.call(post) }
it "sends an email to each of the followers" do
expect(mails.first.to).to eq followers.first.email
expect(mails.last.to).to eq followers.last.email
end
end
The pattern is called service objects. Having a simple object which takes care of a single task is easy to test and will make it easier to implement sending the emails in a background job. I'll leave that part to you.
Further reading:
http://www.sitepoint.com/comparing-background-processing-libraries-sidekiq/
https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services

testing with rspec codeschool level 5 challenge 3

Here is the base question for the test:
Update the spec so that whenever a tweet is created, we verify that email_tweeter is called on the tweet object. ***I can not alter the models, question, or mailer.***
Models:
# tweet.rb
class Tweet < ActiveRecord::Base
belongs_to :zombie
validates :message, presence: true
attr_accessible :message
after_create :email_tweeter
def email_tweeter
ZombieMailer.tweet(zombie, self).deliver
end
private :email_tweeter
end
# zombie.rb
class Zombie < ActiveRecord::Base
has_many :tweets
validates :email, presence: true
attr_accessible :email
end
Mailer:
class ZombieMailer < ActionMailer::Base
def tweet(zombie, tweet)
mail(:from => 'admin#codeschool.com',
:to => zombie.email,
:subject => tweet.message)
end
end
I keep bouncing around on this and could use a few pointers. Here is what I have been working with now: UPDATED
describe Tweet do
context 'after create' do
let(:zombie) { Zombie.create(email: 'anything#example.org') }
let(:tweet) { zombie.tweets.new(message: 'Arrrrgggghhhh') }
it 'calls "email_tweeter" on the tweet' do
tweet.email_tweeter.should_receive(:zombie)
tweet.save
end
end
end
And the error message is:
Failures:
1) Tweet after create calls "email_tweeter" on the tweet
Failure/Error: tweet.email_tweeter.should_receive(:zombie)
NoMethodError:
private method `email_tweeter' called for #<Tweet:0x000000062efb48>
# zombie_spec.rb:7:in `block (3 levels) '
Finished in 0.26328 seconds
1 example, 1 failure
Failed examples:
rspec zombie_spec.rb:6 # Tweet after create calls "email_tweeter" on the tweet
Any rspec peeps out there can point me in the right direction as to what I am missing here? Thank you.
How about this:
it 'calls "email_tweeter" on the tweet' do
tweet.should_receive(:email_tweeter)
tweet.save
end
do this
it 'calls "email_tweeter" on the tweet' do
tweet.email_tweeter.should_receive(:zombie)
tweet.save
end
Remove:
private :email_tweeter
You can't test private methods.
Update:
In fact you can test private methods (with send or eval methods which do not care about privacy), but you shouldn't, as those are part of implementation not the final output. In your tests you should rather save a new tweet an check that email has been sent. implementation details can change with time, it shouldn't affect tests as long as the mail is being send. You can for example try:
it 'generates and sends an email' do
tweet.save
ActionMailer::Base.deliveries.last.message.should eq tweet.message
end

Adding a validation error with a before_save callback or custom validator?

I have a model Listing that belongs_to :user. Alternatively, User has_many :listings. Each listing has a category field that classifies it (dogs, cats, etc). The User also has a boolean field called is_premium.
Here is how I am validating the category...
validates_format_of :category,
:with => /(dogs|cats|birds|tigers|lions|rhinos)/,
:message => 'is incorrect'
Let's say I only want to allow premium users to be able to add tigers, lions, and rhinos. How would I go about this? Would it be best to do it in a before_save method?
before_save :premium_check
def premium_check
# Some type of logic here to see if category is tiger, lion, or rhino.
# If it is, then check if the user is premium. If it's not, it doesn't matter.
# If user isn't premium then add an error message.
end
Thanks in advance!
class Listing < ActiveRecord::Base
validate :premium_category
private
def premium_category
if !user.is_premium && %w(tigers lions rhinos).include?(category))
errors.add(:category, "not valid for non premium users")
end
end
end
If you want to add validation errors doing the before_save you could raise an exception then add the error in the controller like this:
class Listing < ActiveRecord::Base
before_save :premium_category
private
def premium_category
if !user.is_premium && %w(tigers lions rhinos).include?(category))
raise Exceptions::NotPremiumUser, "not valid for non premium users"
end
end
end
Then in your controller do something like:
begin
#listing.update(listing_params)
respond_with(#listing)
rescue Exceptions::NotPremiumUser => e
#listing.errors.add(:base, e.message)
respond_with(#listing)
end
Then in your /lib folder add a class like this:
module Exceptions
class NotPremiumUser < StandardError; end
end
But in your case I think using the validate method is a better solution.
Cheers,
You can use validates_exclusion_of:
validates :category, :exclusion => {
:in => ['list', 'of', 'invalid'],
:message => 'must not be premium category',
:unless => :user_is_premium?
}
protected
def user_is_premium?
self.user.premium?
end

rspec testing has_many :through and after_save

I have an (I think) relatively straightforward has_many :through relationship with a join table:
class User < ActiveRecord::Base
has_many :user_following_thing_relationships
has_many :things, :through => :user_following_thing_relationships
end
class Thing < ActiveRecord::Base
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
end
class UserFollowingThingRelationship < ActiveRecord::Base
belongs_to :thing
belongs_to :user
end
And these rspec tests (I know these are not necessarily good tests, these are just to illustrate what's happening):
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
#thing = Thing.create!(:name => "Foo")
#user.things << #thing
end
it "should have created a relationship" do
UserFollowingThingRelationship.first.user.should == #user
UserFollowingThingRelationship.first.thing.should == #thing
end
it "should have followers" do
#thing.followers.should == [#user]
end
end
This works fine UNTIL I add an after_save to the Thing model that references its followers. That is, if I do
class Thing < ActiveRecord::Base
after_save :do_stuff
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def do_stuff
followers.each { |f| puts "I'm followed by #{f.name}" }
end
end
Then the second test fails - i.e., the relationship is still added to the join table, but #thing.followers returns an empty array. Furthermore, that part of the callback never gets called (as if followers is empty within the model). If I add a puts "HI" in the callback before the followers.each line, the "HI" shows up on stdout, so I know the callback is being called. If I comment out the followers.each line, then the tests pass again.
If I do this all through the console, it works fine. I.e., I can do
>> t = Thing.create!(:name => "Foo")
>> t.followers # []
>> u = User.create!(:name => "Bar")
>> u.things << t
>> t.followers # [u]
>> t.save # just to be super duper sure that the callback is triggered
>> t.followers # still [u]
Why is this failing in rspec? Am I doing something horribly wrong?
Update
Everything works if I manually define Thing#followers as
def followers
user_following_thing_relationships.all.map{ |r| r.user }
end
This leads me to believe that perhaps I am defining my has_many :through with :source incorrectly?
Update
I've created a minimal example project and put it on github: https://github.com/dantswain/RspecHasMany
Another Update
Thanks a ton to #PeterNixey and #kikuchiyo for their suggestions below. The final answer turned out to be a combination of both answers and I wish I could split credit between them. I've updated the github project with what I think is the cleanest solution and pushed the changes: https://github.com/dantswain/RspecHasMany
I would still love it if someone could give me a really solid explanation of what is going on here. The most troubling bit for me is why, in the initial problem statement, everything (except the operation of the callback itself) would work if I commented out the reference to followers.
I've had similar problems in the past that have been resolved by reloading the association (rather than the parent object).
Does it work if you reload thing.followers in the RSpec?
it "should have followers" do
#thing.followers.reload
#thing.followers.should == [#user]
end
EDIT
If (as you mention) you're having problems with the callbacks not getting fired then you could do this reloading in the object itself:
class Thing < ActiveRecord::Base
after_save { followers.reload}
after_save :do_stuff
...
end
or
class Thing < ActiveRecord::Base
...
def do_stuff
followers.reload
...
end
end
I don't know why RSpec has issues with not reloading associations but I've hit the same types of problems myself
Edit 2
Although #dantswain confirmed that the followers.reload helped alleviate some of the problems it still didn't fix all of them.
To do that, the solution needed a fix from #kikuchiyo which required calling save after doing the callbacks in Thing:
describe Thing do
before :each do
...
#user.things << #thing
#thing.run_callbacks(:save)
end
...
end
Final suggestion
I believe this is happening because of the use of << on a has_many_through operation. I don't see that the << should in fact trigger your after_save event at all:
Your current code is this:
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
#thing = Thing.create!(:name => "Foo")
#user.things << #thing
end
end
class Thing < ActiveRecord::Base
after_save :do_stuff
...
def do_stuff
followers.each { |f| puts "I'm followed by #{f.name}" }
end
end
and the problem is that the do_stuff is not getting called. I think this is the correct behaviour though.
Let's go through the RSpec:
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
# user is created and saved
#thing = Thing.create!(:name => "Foo")
# thing is created and saved
#user.things << #thing
# user_thing_relationship is created and saved
# no call is made to #user.save since nothing is updated on the user
end
end
The problem is that the third step does not actually require the thing object to be resaved - its simply creating an entry in the join table.
If you'd like to make sure that the #user does call save you could probably get the effect you want like this:
describe Thing do
before(:each) do
#thing = Thing.create!(:name => "Foo")
# thing is created and saved
#user = User.create!(:name => "Fred")
# user is created BUT NOT SAVED
#user.things << #thing
# user_thing_relationship is created and saved
# #user.save is also called as part of the addition
end
end
You may also find that the after_save callback is in fact on the wrong object and that you'd prefer to have it on the relationship object instead. Finally, if the callback really does belong on the user and you do need it to fire after creating the relationship you could use touch to update the user when a new relationship is created.
UPDATED ANSWER **
This passes rspec, without stubbing, running callbacks for save (after_save callback included ), and checks that #thing.followers is not empty before trying to access its elements. (;
describe Thing do
before :each do
#user = User.create(:name => "Fred");
#thing = Thing.new(:name => 'Foo')
#user.things << #thing
#thing.run_callbacks(:save)
end
it "should have created a relationship" do
#thing.followers.should == [#user]
puts #thing.followers.inspect
end
end
class Thing < ActiveRecord::Base
after_save :some_function
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def some_function
the_followers = followers
unless the_followers.empty?
puts "accessing followers here: the_followers = #{the_followers.inspect}..."
end
end
end
ORIGINAL ANSWER **
I was able to get things to work with the after_save callback, so long as I did not reference followers within the body / block of do_stuff. Do you have to reference followers in the real method you are calling from after_save ?
Updated code to stub out callback. Now model can remain as you need it, we show #thing.followers is indeed set as we expected, and we can investigate the functionality of do_stuff / some_function via after_save in a different spec.
I pushed a copy of the code here: https://github.com/kikuchiyo/RspecHasMany
And spec passing thing* code is below:
# thing_spec.rb
require 'spec_helper'
describe Thing do
before :each do
Thing.any_instance.stub(:some_function) { puts 'stubbed out...' }
Thing.any_instance.should_receive(:some_function).once
#thing = Thing.create(:name => "Foo");
#user = User.create(:name => "Fred");
#user.things << #thing
end
it "should have created a relationship" do
#thing.followers.should == [#user]
puts #thing.followers.inspect
end
end
# thing.rb
class Thing < ActiveRecord::Base
after_save :some_function
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def some_function
# well, lets me do this, but I cannot use #x without breaking the spec...
#x = followers
puts 'testing puts hear shows up in standard output'
x ||= 1
puts "testing variable setting and getting here: #{x} == 1\n\t also shows up in standard output"
begin
# If no stubbing, this causes rspec to fail...
puts "accessing followers here: #x = #{#x.inspect}..."
rescue
puts "and this is but this is never seen."
end
end
end
My guess is that you need to reload your Thing instance by doing #thing.reload (I'm sure there's a way to avoid this, but that might get your test passing at first and then you can figure out where you've gone wrong).
Few questions:
I don't see you calling #thing.save in your spec. Are you doing that, just like in your console example?
Why are you calling t.save and not u.save in your console test, considering you're pushing t onto u? Saving u should trigger a save to t, getting the end result you want, and I think it would "make more sense" considering you are really working on u, not t.

How to test a before_save method including accossioations with rspec

I'm having a problem testing the following model:
class Bill < ActiveRecord::Base
belongs_to :consignee
before_save :calc_rate
def calc_rate
self.chargeableweight = self.consignee.destination.rate * self.weight
end
end
The consignee model:
class Consignee < ActiveRecord::Base
belongs_to :destination
has_many :bills
end
The controllers are not touched yet.
The behavior of the app is correct (follow up question: are there any performance problems with that solution?) - but the the test break.
You have a nil object when you didn't
expect it! You might have expected an
instance of Array. The error occurred
while evaluating nil.*
Thank you in advice,
Danny
update:
This bill test breaks using factory girl:
describe Bill do
it "should call the calc_rate method" do
bill = Factory.build(:bill)
bill.save!
bill.should_receive(:calc_rate)
end
end
You have a nil object when you didn't expect it!
Factories:
Factory.define :destination do |f|
f.airport_code "JFK"
end
Factory.define :consignee do |f|
...
f.association :destination
end
Factory.define :bill do |f|
f.association :consignee
f.weight 10
f.chargeableweight 20.0
f.after_create do |bill|
bill.calc_rate
end
describe Consignee do
it "should calculate the rate" do
#pending
#make sure this spec is passing first, so you know your calc_rate method is fine.
end
it "should accept calc_rate before save" do
cosignee = mock("Consignee")
consignee.should_receive(:calc_rate).and_return(2) # => stubbing your value
end
end
I didn't spool up a rails app to test this code, but this should get you close. also, assuming that the columns chargeable_rate, weight, etc are columns on the model, you dont need to call self. Ruby will implicitly expect self if there is no instance method or variable of that name available it will automatically look for class methods.

Resources