I have the following task:
My mailer has multiple actions/templates, each with the same static hardcoded subject.
In my RSpec expectation I would like to verify that certain block triggers email(changes ActionMailer::Base.deliveries.last&.something) of specific mailer action/template.
Limitations:
can not rely on a delivery subject/title(not unique)
preferably avoid checking delivery content(they are mostly the same except for a few minor wording changes)
can not change a delivery subject/title generation just to make this spec pass
can not mock/stub mailer(I don't want to skip the rendering part, this is also a part of the test)
Methods that I tried that didn't work:
ActionMailer::Base.deliveries.last.template_model
# => nil
ActionMailer::Base.deliveries.last.template_alias
# => nil
ActionMailer::Base.deliveries.last.filename
# => nil
ActionMailer::Base.deliveries.last.metadata
# => {}
ActionMailer::Base.deliveries.last.action
# => nil
What might be an idiomatic Rails way of approaching this task? Custom header with action/template name?
Mail::Message methods(for the reference):
ActionMailer::Base.deliveries.last.public_methods(false)
=> [:perform_deliveries=,
:mime_version=,
:sender,
:metadata=,
:boundary,
:multipart?,
:errors,
:to_yaml,
:delivery_handler,
:tag,
:raise_delivery_errors,
:register_for_delivery_notification,
:inform_observers,
:inform_interceptors,
:in_reply_to=,
:comments=,
:in_reply_to,
:reply_to,
:references=,
:message_id=,
:raw_source,
:set_envelope,
:filename,
:raw_envelope,
:envelope_from,
:envelope_date,
:content_description,
:content_description=,
:content_disposition=,
:content_id=,
:content_location,
:content_location=,
:content_transfer_encoding,
:content_transfer_encoding=,
:transport_encoding,
:transport_encoding=,
:received,
:received=,
:header=,
:reply_to=,
:resent_bcc,
:resent_bcc=,
:resent_cc,
:resent_cc=,
:resent_date,
:resent_date=,
:resent_from,
:resent_from=,
:bcc,
:resent_message_id=,
:resent_message_id,
:resent_sender=,
:resent_sender,
:resent_to=,
:resent_to,
:references,
:from,
:return_path=,
:sender=,
:return_path,
:smtp_envelope_from=,
:smtp_envelope_to=,
:smtp_envelope_from,
:body_encoding,
:body_encoding=,
:smtp_envelope_to,
:cc_addrs,
:bcc_addrs,
:from_addrs,
:has_message_id?,
:has_date?,
:header_fields,
:has_charset?,
:has_content_transfer_encoding?,
:has_mime_version?,
:has_transfer_encoding?,
:destinations,
:add_date,
:add_message_id,
:add_mime_version,
:sub_type,
:to,
:subject,
:subject=,
:cc=,
:bcc=,
:add_content_transfer_encoding,
:perform_deliveries,
:<=>,
:message_content_type,
:delivery_status_part,
:add_content_type,
:add_charset,
:==,
:main_type,
:transfer_encoding,
:diagnostic_code,
:mime_parameters,
:[],
:[]=,
:remote_mta,
:retryable?,
:html_part=,
:bounced?,
:final_recipient,
:add_transfer_encoding,
:text_part=,
:convert_to_multipart,
:attachment,
:keywords,
:charset=,
:delivery_handler=,
:without_attachments!,
:error_status,
:delivery_status_report?,
:decode_body,
:all_parts,
:skip_deletion,
:mark_for_delete=,
:is_marked_for_delete?,
:inspect,
:multipart_report?,
:method_missing,
:attachment?,
:mime_version,
:add_file,
:action,
:raise_delivery_errors=,
:parts,
:to_s,
:body=,
:content_type_parameters,
:has_attachments?,
:add_part,
:find_first_mime_type,
:encoded,
:date,
:decoded,
:attachments,
:track_links,
:part,
:templated?,
:metadata,
:export_headers,
:track_opens,
:template_alias,
:template_model,
:message_stream,
:export_attachments,
:body_html,
:body_text,
:message_id,
:comments,
:to_addrs,
:from=,
:postmark_response,
:default,
:to_postmark_hash,
:content_disposition,
:content_id,
:has_content_type?,
:keywords=,
:delivery_method,
:date=,
:text?,
:cc,
:delivered=,
:postmark_response=,
:encode!,
:mime_type,
:html?,
:tag=,
:delivered,
:delivered?,
:track_links=,
:track_opens=,
:postmark_attachments=,
:postmark_attachments,
:read,
:message_stream=,
:prerender,
:text_part,
:html_part,
:reply,
:ready_to_send!,
:template_model=,
:deliver!,
:headers,
:header,
:content_type,
:to=,
:charset,
:deliver,
:body,
:content_type=]
Related question:
Rails, RSpec: How to test, that a specific mailer action gets triggered (uses mocks)
That's what I currently ended up with(until I find a better way):
added the following line to the each of related mailer actions:
headers['X-Rails-Template'] = __method__
add my specs looks a bit like this:
subject { -> { described_class.call } }
let(:last_template_name) do
-> do
m = ActionMailer::Base.deliveries.last
next if m.blank?
m.header['X-Rails-Template']&.value
end
end
it do
is_expected.to change { ActionMailer::Base.deliveries.count }.to(1)
.and change(&last_template_name).to('mailer_action_name1')
end
The problem
The problem is that I know the controller sort action is being called, and that it's getting the right params to do its thing. I can see everything is working when I test through the UI, but it won't sort for my tests. I can see (using "puts" in the controller action) that the hash is being sent to the controller action correctly, so that's working well. The issue is that it seems like the controller action simply isn't... ACTING.
The question
How do I test this? I've tried inserting a "sleep 2" after the :post, :sort line in my spec. You can see I've added the "xhr" in front of that line, and I've tried adding :format => :js at the end of that line (not shown) to no avail.
What I'm doing
I'm using JQuery UI's "sortable" plugin to sort jobs on a checklist on-screen. It currently works well, and I can "test" this by just changing the order on screen, and then refreshing the page to see that everything is now in the correct spot (the spot I just moved things to before refreshing). Obviously, this isn't rigorous testing, but it's been good enough until now.
Now, I'm adding some logic to copy/archive the sorted items in the background, and I need to do some testing to make sure all the data is being manipulated properly when I do a sort. The old sort-refresh-check testing method won't work here because some stuff happens in the background, and that stuff doesn't show up on screen. Before I write those tests, I wanted to write tests for the currently-working setup, but I can't get the tests to pass.
Notes on associations
There are Jobs, Checklists, and ChecklistsJobs (the join table). The join table has the "job_position" field that's being updated using this sort. See below in the "code" section for snippets of the models.
Resources used to do this
JQuery UI - Sortable
Railscast #147 Sortable Lists (revised)
Guard gem
Rails 3.2.11
ruby 1.9.3p374
rspec 2.13.1
Gems in :test
gem "rspec-rails", :group => [:test, :development]
group :test do
gem "factory_girl_rails", :require => false
gem "capybara"
gem "guard-rspec"
gem 'rb-fsevent', '~> 0.9'
end
Code
job.rb
class Job < ActiveRecord::Base
scope :archived_state, lambda {|s| where(:archived => s)}
belongs_to :creator, :class_name => "Admin", :foreign_key => "creator_id"
has_many :checklists_jobs, :dependent => :destroy
has_many :checklists, :through => :checklists_jobs
.
.
.
end
checklist.rb
class Checklist < ActiveRecord::Base
scope :archived_state, lambda {|s| where(:archived => s) }
has_many :checklists_jobs, :dependent => :destroy, :order => 'checklists_jobs.job_position'#, :conditions => {'archived_at' => nil}
has_many :jobs, :through => :checklists_jobs
.
.
.
end
checklists_job.rb
class ChecklistsJob < ActiveRecord::Base
belongs_to :job
belongs_to :checklist
attr_accessible :job_position, :job_required
.
.
.
end
application.js
$(function() {
$('ul[id^="sortable"]').sortable({
axis: 'y',
handle: '.handle',
update: function() {
return $.post($(this).data('update-url'), $(this).sortable('serialize'));
}
});
});
Show.html.erb view
<dd><ul class="unstyled" id="sortable" data-update-url="<%= sort_checklist_path(#checklist) %>">
<% #checklist.checklists_jobs.archived_state(:false).each do |cj| %>
<%= content_tag_for :li, cj do %><span class="handle"><i class="icon-move btn btn-mini"></i></span> <strong><%= cj.job.name %></strong><% if cj.job.description? %> - <%= cj.job.description %><% end %>
<% end %>
<% end %>
</ul></dd>
checklists_controller.rb
def sort
#checklist = Checklist.find(params[:id])
params[:checklists_job].each_with_index do |id, index|
#checklist.checklists_jobs.update_all({job_position: index+1}, {id: id})
end
render nothing: true
end
checklists_controller_spec.rb - I know this isn't super efficient user of space, but I broke everything out into individual lines to make sure I was setting everything up correctly.
require 'spec_helper'
describe ChecklistsController do
login_admin
describe "POST sort" do
context "with no submission" do
before do
#checklist = FactoryGirl.create(:checklist)
#job1 = FactoryGirl.create(:job)
#job2 = FactoryGirl.create(:job)
#job3 = FactoryGirl.create(:job)
#job4 = FactoryGirl.create(:job)
#checklist.jobs << [#job1, #job2, #job3, #job4]
#checklist.save
#cj1 = #checklist.checklists_jobs.find_by_job_id(#job1.id)
#cj2 = #checklist.checklists_jobs.find_by_job_id(#job2.id)
#cj3 = #checklist.checklists_jobs.find_by_job_id(#job3.id)
#cj4 = #checklist.checklists_jobs.find_by_job_id(#job4.id)
#cj1.job_position = 1
#cj2.job_position = 2
#cj3.job_position = 3
#cj4.job_position = 4
#cj1.save
#cj2.save
#cj3.save
#cj4.save
#attr = [#job4.id, #job3.id, #job2.id, #job1.id]
end
# format of the params hash that's being passed:
# {"checklists_job"=>["545", "544", "546", "547"], "id"=>"124"}
it "sorts the checklist's checklists_jobs" do
xhr :post, :sort, { :id => #checklist.id, :checklists_job => #attr }
#checklist.reload
# The test fails on the next line
#checklist.checklists_jobs.find_by_job_id(#job1.id).job_position.should == 4
end
end
end
end
Test results - Note that I've marked the failing line in the spec above
Failures:
1) ChecklistsController POST sort with no submission sorts the checklist's checklists_jobs
Failure/Error: #checklist.checklists_jobs.find_by_job_id(#job1.id).job_position.should == 4
expected: 4
got: 1 (using ==)
# ./spec/controllers/checklists_controller_spec.rb:394:in `block (4 levels) in <top (required)>'
Finished in 2.59 seconds
51 examples, 1 failure
Failed examples:
rspec ./spec/controllers/checklists_controller_spec.rb:391 # ChecklistsController POST sort with no submission sorts the checklist's checklists_jobs
This is a pretty simple answer: The #attr array I was building in my before block was an array of job ids, and should've been an array of checklists_job ids.
This:
#attr = [#job4.id, #job3.id, #job2.id, #job1.id]
Should've been this:
#attr = [#cj4.id, #cj3.id, #cj2.id, #cj1.id]
Oops.
I would like this before filter to run every time the page is loaded (for now) to check if an item is over 7 days old or not and if so, run some actions on it to update its attributes.
I have before_filter :update_it in the application controller. update_it is defined below that in the same controller as:
def update_it
#books = Book.all
#books.each do |book|
book.update_queue
end
end
Then update_queue is defined in the book model. Here's everything in the model that pertains to this:
scope :my_books, lambda {|user_id|
{:conditions => {:user_id => user_id}}
}
scope :reading_books, lambda {
{:conditions => {:reading => 1}}
}
scope :latest_first, lambda {
{:order => "created_at DESC"}
}
def move_from_queue_to_reading
self.update_attributes(:queued => false, :reading => 1);
end
def move_from_reading_to_list
self.update_attributes(:reading => 0);
end
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
# If been 7 days since last 'currently reading' book created
if days_gone >= 7
# If there's a queued book, move it to 'currently reading'
if Book.my_books(user_id).where(:queued => true)
new_book = Book.my_books(user_id).latest_first.where(:queued => true).last
new_book.move_from_queue_to_reading
currently_reading = Book.my_books(user_id).reading_books.last
currently_reading.move_from_reading_to_list
# Otherwise, create a new one
else
Book.my_books(user_id).create(:title => "Sample book", :reading => 1)
end
end
end
My relationship is that a book belongs_to a user and a user has_many books. I'm showing these books in the view through the user show view, not that it matters though.
So the errors I keep getting are that move_from_queue_to_reading and move_from_reading_to_list are undefined methods. How can this be? I'm clearly defining them and then calling them below. I really am at a loss and would greatly appreciate some insight into what I'm doing wrong. I'm a beginner here, so any structured criticism would be great :)
EDIT
The exact error message I get and stack trace is as follows:
NoMethodError in UsersController#show
undefined method `move_from_queue_to_reading' for nil:NilClass
app/models/book.rb:41:in `update_queue'
app/controllers/application_controller.rb:22:in `block in update_it'
app/controllers/application_controller.rb:21:in `each'
app/controllers/application_controller.rb:21:in `update_it'
I suspect that the collection returned is an empty array (which is still 'truthy' when tested). So calling .last is returning nil to the new_book and currently_reading local variables. Try changing:
if Book.my_books(user_id).where(:queued => true)
to:
if Book.my_books(user_id).where(:queued => true).exists?
Additionally, you are modifying the scope when finding currently_reading. This can potentially cause the query to again return no results. Change:
currently_reading.move_from_reading_to_list
to:
currently_reading.move_from_reading_to_list if currently_reading
I use Rails 3.0.6 with mongoID 2.0.2. Recently I encountered an issue with save! method when overriding setter (I am trying to create my own nested attributes).
So here is the model:
class FeedItem
include Mongoid::Document
has_many :audio_refs
def audio_refs=(attributes_array, binding)
attributes_array.each do |attributes|
if attributes[:audio_track][:id]
self.audio_refs.build(:audio_track => AudioTrack.find(attributes[:audio_track][:id]))
elsif attributes[:audio_track][:file]
self.audio_refs.build(:audio_track => AudioTrack.new(:user_id => attributes[:audio_track][:user_id], :file => attributes[:audio_track][:file]))
end
end
if !binding
self.save!
end
end
AudioRef model (which is just buffer between audio_tracks and feed_items) is:
class AudioRef
include Mongoid::Document
belongs_to :feed_item
belongs_to :audio_track
end
And AudioTrack:
class AudioTrack
include Mongoid::Document
has_many :audio_refs
mount_uploader :file, AudioUploader
end
So here is the spec for the FeedItem model which doesn`t work:
it "Should create audio_track and add audio_ref" do
#audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3"))
#feed_item= FeedItem.new(
:user => #author,
:message => {:body => Faker::Lorem.sentence(4)},
:audio_refs => [
{:audio_track => {:id => #audio_track.id}},
{:audio_track => {:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}}
]
)
#feed_item.save!
#feed_item.reload
#feed_item.audio_refs.length.should be(2)
end
As you can see, the reason I am overriding audio_refs= method is that FeedItem can be created from existing AudioTracks (when there is params[:audio_track][:id]) or from uploaded file (params[:audio_track][:file]).
The problem is that #feed_item.audio_refs.length == 0 when I run this spec, i.e. audio_refs are not saved. Could you please help me with that?
Some investigation:
1) binding param is "true" by default (this means we are in building mode)
I found a solution to my problem but I didnt understand why save method doesnt work and didn`t make my code work. So first of all let me describe my investigations about the problem. After audio_refs= is called an array of audio_refs is created BUT in any audio_ref is no feed_item_id. Probably it is because the feed_item is not saved by the moment.
So the solution is quite simple - Virtual Attributes. To understand them watch corresponding railscasts
So my solution is to create audio_refs by means of callback "after_save"
I slightly changed my models:
In FeedItem.rb I added
attr_writer :audio_tracks #feed_item operates with audio_tracks array
after_save :assign_audio #method to be called on callback
def assign_audio
if #audio_tracks
#audio_tracks.each do |attributes|
if attributes[:id]
self.audio_refs << AudioRef.new(:audio_track => AudioTrack.find(attributes[:id]))
elsif attributes[:file]
self.audio_refs << AudioRef.new(:audio_track => AudioTrack.new(:user_id => attributes[:user_id], :file => attributes[:file]))
end
end
end
end
And the spec is now:
it "Should create audio_track and add audio_ref" do
#audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3"))
#feed_item= FeedItem.new(
:user => #author,
:message => {:body => Faker::Lorem.sentence(4)},
:audio_tracks => [
{:id => #audio_track.id},
{:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}
]
)
#feed_item.save!
#feed_item.reload
#feed_item.audio_refs.length.should be(2)
end
And it works fine!!! Good luck with your coding)
Check that audio_refs=() is actually being called, by adding debug output of some kind. My feeling is that your FeedItem.new() call doesn't use the audio_refs=() setter.
Here's the source code of the ActiveRecord::Base#initialize method, taken from APIdock:
# File activerecord/lib/active_record/base.rb, line 1396
def initialize(attributes = nil)
#attributes = attributes_from_column_definition
#attributes_cache = {}
#new_record = true
#readonly = false
#destroyed = false
#marked_for_destruction = false
#previously_changed = {}
#changed_attributes = {}
ensure_proper_type
populate_with_current_scope_attributes
self.attributes = attributes unless attributes.nil?
result = yield self if block_given?
_run_initialize_callbacks
result
end
I don't currently have an environment to test this, but it looks like it's setting the attributes hash directly without going through each attribute's setter. If that's the case, you'll need to call your setter manually.
Actually, I think the fact you're not getting an exception for the number of arguments (binding not set) proves that your setter isn't being called.