I have a rails method which allows a user to submit a review and to counterparty, it sends an email using the delayed jobs.
def update_review
#review.add_review_content(review_params)
ReviewMailer.delay.review_posted(#review.product_owner, params[:id])
end
And I am trying to add a rspec test for this to check if the mailer is delivered properly and to whom. The delayed jobs run immediately after they are created on test because I want other jobs like the job to update the product owners overall rating to be completed immediately.
So the email does get fired but how can I add a test for it?
EDIT: Adding current tests
My current tests are:
describe 'PUT #update the review' do
let(:attr) do
{ rating: 3.0, raw_review: 'Some example review here' }
end
before(:each) do
#review = FactoryBot.create :review
put :update, id: #review.id, review: attr
end
it 'creates a job' do
ActiveJob::Base.queue_adapter = :test
expect {
AdminMailer.review_posted(#coach, #review.id).deliver_later
}.to have_enqueued_job
end
it { should respond_with 200 }
end
This does test that the mailer works properly but I want to test that it gets triggered properly in the method flow as well.
It sounds like what you want is to ensure that the update_review method enqueues a job to send the correct email to the correct recipient. Here's a simpler way to accomplish that:
describe 'PUT #update the review' do
let(:params) { { rating: rating, raw_review: raw_review } }
let(:rating) { 3.0 }
let(:raw_review) { 'Some example review here' }
let(:review) { FactoryBot.create(:review) }
let(:delayed_review_mailer) { instance_double(ReviewMailer) }
before do
# assuming this is how the controller finds the review...
allow(Review).to receive(:find).and_return(review)
# mock the method chain that enqueues the job to send the email
allow(ReviewMailer).to receive(:delay).and_return(delayed_review_mailer)
allow(delayed_review_mailer).to receive(:review_posted)
put :update, id: review.id review: params
end
it 'adds the review content to the review' do
review.reload
expect(review.rating).to eq(rating)
expect(review.raw_review).to eq(raw_review)
end
it 'sends a delayed email' do
expect(ReviewMailer).to have_received(:delay)
end
it 'sends a review posted email to the product owner' do
expect(delayed_review_mailer)
.to have_received(:review_posted)
.with(review.product_owner, review.id)
end
end
The reason I prefer this approach is that a) it could be done without touching the database at all (by swapping the factory for an instance double), and b) it doesn't try to test parts of Rails that were already tested by the folks who built Rails, like ActiveJob and ActionMailer. You can trust Rails' own unit tests for those classes.
Related
We can check enqueued_jobs.length, provided that email is the only possible type of background job.
it 'sends exactly one, particular email' do
expect { post :create }.to(
# First, we can check about the particular email we care about.
have_enqueued_mail(MyMailer, :my_particular_email)
)
# But we also have to check that no other email was sent.
expect(ActiveJob::Base.queue_adapter.enqueued_jobs.length).to eq(1)
end
Is there a better way to assert that:
MyMailer.my_particular_email was enqueued,
no other email was enqueued,
and we don't care if other, non-email background jobs were enqueued
I believe once will work with this matcher.
expect {
post :create
}.to have_enqueued_mail(MyMailer, :my_particular_email).once
# first filter MyMailer job
my_mail_jobs = ActiveJob::Base.queue_adapter.enqueued_jobs.select { |job|
job[:job] == SendEmailsJob &&
job[:args][0] == "MyMailer"
}
# check only once
expect(my_mail_jobs.length).to eq(1)
# and that send to your particular email not other email
expect(my_mail_jobs.first[:args]).to include("your particular email")
My chat app has a chat class and a message class; when a message is added to the chat, chat.updated_at should also be updated (achieved with belongs_to :chat, touch: true).
When I test this manually, it works correctly, the time is updated. My test below fails however, and I cannot work out why.
test "sending a message should update chats updated timestamp" do
sign_in #user
assert_changes "#chat.updated_at" do
post messages_path(params: { message: {
text: 'Hello', to_id: #bob.id, chat_id: #chat.id
}})
assert_response :success
end
end
I simply get the error #chat.updated_at didn't change.
My chat fixture is
one:
id: 1
subject: nil
updated_at: <%= 2.hours.ago %>
I think you should use model.reload https://apidock.com/rails/ActiveRecord/Persistence/reload
assert_changes "#chat.reload.updated_at" do
Explanation:
Once a Rails model is loaded from DB, when you access an attribute it will use the values that were already read and not make the same query again and again (unless explicitly told to do so with reload). And in your test, Ruby simply compares #chat.updated_at before and after but there is no second query on the second time, simply a cached attribute
You need to reload the record from the database to get the object in your test to reflect the changes that were performed in the database:
test "sending a message should update chats updated timestamp" do
sign_in #user
assert_changes "#chat.updated_at" do
post messages_path(params: { message: {
text: 'Hello', to_id: #bob.id, chat_id: #chat.id
}})
#chat.reload
assert_response :success
end
end
Remember that #chat in your test and controller point to completely different objects in memory.
I have a broken ActionMailer::Base test that at the moment confounds me. Perhaps I am not seeing what the error is but when I reach my assertion that checks that an email's to: field is correctly shown, I receive the error in question. Below is a snippet of my code. I am unsing MiniTest::Spec::Test with Factory Girl. Below is my Factory Girl code and test condition. I have outlined the problem areas and have illustrated what the conditions would print out on the console.
describe PostsController do
let(:forum) { create(:forum) }
let(:user) { create(:account) }
let(:admin) { create(:admin) }
let(:topic) { create(:topic) }
let(:post_object) { create(:post) }
before { ActionMailer::Base.deliveries.clear }
#code ------------
it 'create action: user2 replying to user1 receives
a creation email while user1 receives a reply email' do
login_as user
assert_difference('ActionMailer::Base.deliveries.size', 2) do
post :create, topic_id: post_object.topic.id, post: { body:
'Post reply gets sent to User 1. Post creation gets sent to User 2' }
end
email = ActionMailer::Base.deliveries
# -----debugging area ----------
puts email.first.to 'user1#gmail.com'
puts email.last.to 'user2#gmail.com'
puts email.map(&:to) 'user1#gmail.com, user2#gmail.com'
# ------debugging area --------
#-------failing test ----------
email.first.to.must_equal post_object.topic.account.email
Expected 'comesoutofnowhereuser#gmail.com, Actual: user1#gmail.com'
#------failing test -----------
email.first.subject.must_equal 'Someone has responded to your post'
email.last.to.must_equal user.email
email.last.subject.must_equal 'Post successfully created'
must_redirect_to topic_path(post_object.topic.id)
end
I don't know why crazyuser#gmail.com is to be expected. I'm wondering if there is any change after the assert_difference block but I've also tried running puts and debugger statements in the block and I get the same results. Nothing changes coming out of assert_difference. Some help would be greatly appreciated. Thanks.
.to should give you an Array of email addresses, so test that the email is included in the array with must_include:
email.last.to.must_include user.email
The answer to this problem was that my factories were off. The associations among my factory data were being dynamically generated on the fly to different data. Overriding the factory data solved this problem.
I have a Mailer that looks something like this:
class EventMailer < BaseMailer
def event_added(user_id, event_id)
# do stuff and email user about the event
end
end
I'm calling this EventMailer like this from inside the Event class:
class Event < Task
def notify_by_email(user)
EmailLog.send_once(user.id, id) do
EventMailer.delay(queue: 'mailer').event_added(user.id, id)
end
end
end
where EmailLog is a class that logs sent emails. .delay is added by Sidekiq.
But when I try to test that #notify_by_email is called only once per event and user, my spec fails:
1) Event#notify_by_email only sends once per user
Failure/Error: expect(EventMailer).to receive(:event_added).once
(<EventMailer (class)>).event_added(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
The spec looks like:
let(:owner) { User.make! }
let(:product) { Product.make! }
let(:event) { Task.make!(user: owner, product: product) }
describe '#notify_by_email' do
before do
EventMailer.stub(:delay).and_return(EventMailer)
end
it 'only sends once per user' do
event.notify_by_email(owner)
event.notify_by_email(owner)
expect(EventMailer).to receive(:event_added).once
end
end
Any insights into why this spec is failing and how I can fix it? Strangely, if I put a puts statement inside the block that's passed to EmailLog.send_once, it prints only once, the spec still reports that EventMailer.event_added wasn't called.
Your expectation should be declared before the code you're testing. Using expect(...).to receive(...) basically means "this message should be received between now and the end of this spec". Because the expectation is the last line of your spec, it fails.
Try moving it before and you should be good to go:
it 'only sends once per user' do
expect(EventMailer).to receive(:event_added).once
event.notify_by_email(owner)
event.notify_by_email(owner)
end
When writing tests using RSpec, I regularly have the need to express something like
Klass.any_instance_with_id(id).expects(:method)
Main reason is that in my test, I often have the object that should receive that method call available, but due to the fact that ActiveRecord, when loading the object with that id from the database, will create a different instance, I can't put the "expects" on my own instance
Sometimes I can stub the find method to force ActiveRecord to load my instance, sometimes I can stub other methods, but having that "any_instance_with_id" would make life so much easier...
Can't image I'm the first having this problem... So if any of you found a "workaround", I'd be glad to find out!
Example illustrating the need:
controller spec:
describe 'an authorized email' do
let(:lead) { create(:lead, status: 'approved') }
it "should invoice its organisation in case the organisation exceeds its credit limit" do
lead.organisation.expects :invoice_leads
get :email
end
end
controller:
def email
leads = Lead.approved
leads.each do |lead|
lead.organisation.invoice_leads if lead.organisation.credit_limit_exceeded?
end
redirect_to root_path
end
It seems weird to me you need that for specs.
You should take the problem one level higher: when your app tries to retrieve the record.
Example:
#code
#user = User.find(session[:user_id])
# spec
let(:fake_user) { mock_model 'User', method: false }
it 'description' do
User.should_receive(:find).and_return fake_user
fake_user.expects(:method)
#...
end
Order/invoice example:
let(:order) { mock_model 'order', invoice: invoice }
let(:invoice) { mock_model 'Invoice', 'archive!' => false }