I'm currently changing our rails mailers to use the newer way of using the mailer that uses parameterization, which brings our code base inline with the rails guide, but more importantly it also allows the parameters to be filtered appropriately in the logs and 3rd party apps like AppSignal.
ie. I'm changing this
UserMailer.new_user_email(user).deliver_later
to
UserMailer.with(user: user).new_user_email.deliver_later
But we have a quite a few specs that use Rspec Mocks to confirm that a mailer was called with the appropriate params. Generally these test that a controller actually asked the mailer to spend the email correctly.
We generally have something like:
expect(UserMailer).to receive(:new_user_email)
.with(user)
.and_return(OpenStruct.new(deliver_later: true))
.once
But now with the parameterization of the mailer, I don't see any easy way to use rspec mocks to verify that the correct mailer method was called with the correct params. Does anyone have any ideas on how best to test this now? Readability of the expectation is probably the biggest factor here, ideally it is one line without multiple lines of mocking setup.
Note: that I don't really want to actually run the mailer, we have mailer unit specs that test the actual mailer is working.
When you have couple of methods which are chained you can use receive_message_chain
But there is one backdraw - it doesn't support the whole fluent interface of counters like once twice
So you have to do one trick here:
# set counter manually
counter = 0
expect(UserMailer).to receive_message_chain(
:with, :new_user_email
).with(user: user).with(no_args).and_return(OpenStruct.new(deliver_later: true)) do
counter += 1
end
# Very important: Here must be call of your method which triggers `UserMailer` mailer. For example
UserNotifier.notify_user(user)
expect(counter).to eq(1)
# class for example
class UserNotifier
def self.notify_user(user)
UserMailer.with(user: user).new_user_email.deliver_later
end
end
So for anyone else that hits this problem in the future. I ended up adding a helper method in the specs/support directory with something like this
def expect_mailer_call(mailer, action, params, delivery_method = :deliver_later)
mailer_double = instance_double(mailer)
message_delivery_double = instance_double(ActionMailer::MessageDelivery)
expect(mailer).to receive(:with).with(params).and_return(mailer_double)
expect(mailer_double).to receive(action).with(no_args).and_return(message_delivery_double)
expect(message_delivery_double).to receive(delivery_method).once
end
Which can then be called in a spec like this
expect_mailer_call(UserMailer, :new_user_email, { to:'email#email.com', name: kind_of(String) })
or for deliver_now
expect_mailer_call(UserMailer, :new_user_email, { to:'email#email.com', name: kind_of(String) }, :deliver_now)
It works good enough for our situation, but you might need to adapt it and add a part for the amount of emails or something if you need to configure the once restriction.
Related
I'm looking to write a controller spec which tests if a method was called on an instance of a model class for ActiveRecord.
For example, there is a model Post and I want to check if the post with the id 55 had the method foobar called on it.
Some ways that almost work:
expect_any_instance_of(Post).to receive(:foobar)
This almost works but it can not check which post the method was called on
using double
This would normally work but in the controller spec, only ids are passed over so I have no way of inserting the double, short of mocking the response from activerecord find
Does rspec provide any tools to check a method was called on a specific model instance?
It is possible but requires much more mocking, and this means that you're testing the implementation details of a controller which is an antipattern.
Assuming your controller is this:
def action(id)
Post.find(params[:id]).foobar
end
You'd need to mock sth like this (I'll ignore RSpec's good practices like having vars in let's, you can easily extract it later when you have a working example):
mock = instance_double(Post)
expect(Post).to receive(:find).with(55).and_return(mock)
expect(mock).to receive(:foobar)
# trigger the action here
But as the comments already stated, it's probably cleaner to test side effects. If your action deletes a Post, check that the post has been deleted
expect { trigger_action params: {id: 55} }.to change(Post, :count).by(-1)
This makes your code less brittle (you can refactor the internals, and the specs for the controller stay green - meaning your app still works as described in those specs).
I am using Rails 5.
I have an Affiliate model, with a boolean attribute email_notifications_on.
I am building a quite robust email drip system for affiliates and can't figure out where the best place is to check if the affiliate has email notifications on before delivering the email.
Most of my emails are being sent from Resque BG jobs, a few others from controllers.
Here is an example of how I am checking the subscribe status from a BG job:
class NewAffiliateLinkEmailer
#queue = :email_queue
def self.perform(aff_id)
affiliate = Affiliate.find(aff_id)
if affiliate.email_notifications_on?
AffiliateMailer.send_links(affiliate).deliver_now
end
end
end
It seems like writing if affiliate.email_notifications_on? in 10+ areas is not the right way to do this, especially if I need another condition to be met in the future. Or is this fine?
I thought maybe some sort of callback in the AffiliteMailer would work, but saw many people advising against business logic in the Mailer.
Any thoughts/advice would be appreciated.
To be honest, I don't think any better way than creating a method in Affiliate model as follows,
def should_send_email?
# all business logic come here
# to start with you will just have following
# email_notifications_on?
# later you can add `&&` or any business logic for more conditions
end
You can use this method instead of the attribute. It is more re-usable and extendable. You will still have to use the method in every call. If you like single liners then you can use lambda.
I have a basic model like the following
class MyModel
def initialize(attrs)
#attrs = attrs
#rest_client = Some::REST::Client.new
end
def do_a_rest_call(some_str)
#rest_client.create_thing(some_str)
end
end
For testing purposes, I don't want #rest_client to make remote calls. Instead, in a test environment, I just want to make sure that #rest_client gets called with a specific some_str when it goes through certain branches of code.
In an ideal world, I'd have an assertion similar to:
expect(my_model_instance).to.receive(do_a_rest_call).with(some_str) where in the test I will pass some_str to make sure it's the right one.
What's the best way to do this using RSpec 3.8 and Rails 5.2.2?
A solution that should work without any additional gems:
let(:rest_client_double) { instance_double(Some::REST::Client, create_thing: response) }
it 'sends get request to the RestClient' do
allow(Some::REST::Client).to receive(:new).and_return(rest_client_double)
MyModel.new(attrs).do_a_rest_call(some_str)
expect(rest_client_duble).to have_received(:create_thing).with(some_str).once
end
Basically, you are creating a double for REST client.
Then, you make sure that when calling Some::REST::Client.new the double will be used (instead of real REST client instance).
Finally, you call a method on your model and check if double received given message.
(I think this question generalises to stubbing any extensively-pinged API, but I'm asking the question based on the code I'm actually working with)
We're using the Contentful Model extensively in our controllers and views including in our layouts. This means that in any feature test where we visit (say) the homepage, our controller action will include something like this:
class HomepageController < ApplicationController
def homepage
# ... other stuff
#homepage_content = Homepage.find ('contentful_entry_id')
end
end
... where Homepage is a subclass of ContentfulModel::Base, and #homepage_content will have various calls on it in the view (sometimes chained). In the footer there's a similar instance variable set and used repeatedly.
So for feature testing this is a pain. I've only come up with two options:
Stub every single call (dozens) on all Contentful model instances, and either stub method chains or ensure they return a suitable mock
or
Use a gem like VCR to store the Contentful responses for every feature spec
Both of these (at least the way I'm doing them) have pretty bad drawbacks:
1) leads to a bunch of test kruft that will have to be updated every time we add or remove a field from the relevant model;
2) means we generate a vcr yaml files for every feature test - and that we have to remember to clear the relevant yml file whenever we change an element of the test that would change the requests it sends
Am I missing a third option? Or is there some sensible way to do either of the above options without getting the main drawbacks?
I'm the maintainer of contentful_model.
We use VCR to stub API Calls, so that you can test with real data and avoid complicated test code.
Cheers
I have an RSpec test like this:
it "should ..." do
# mailer = mock
# mailer.should_receive(:deliver)
Mailer.should_receive(:notification_to_sender)#.and_return(mailer)
visit transactions_path
expect do
page.should_not have_css("table#transactions_list tbody tr")
find('#some_button').click
page.should have_css("table#transactions_list tbody tr", :count => 1)
end.to change{Transaction.count}.by(1)
end
If I remove the commented pieces at the top, the test passes. But with the commented sections in place (how I'd expect to write it) the test fails.
I got the commented pieces off some of googling around the net, but I don't really understand what it's doing or why this fixes it. It seems like there should be a cleaner way to test emails without this.
Can anyone shed some light? Thanks!
I'm using rails 3 and rspec-rails 2.10.1
I think you want an instance of Mailer to receive notification_to_sender not the class. From the Rails API
You never instantiate your mailer class. Rather, your delivery instance methods are automatically wrapped in class methods that start with the word deliver_ followed by the name of the mailer method that you would like to deliver. The signup_notification method defined above is delivered by invoking Notifier.deliver_signup_notification.
Therefore I would use
Mailer.any_instance.should_receive(:notification_to_sender)
Also, if you need to get the last delivered message, use
ActionMailer::Base.deliveries.last
I think that should solve your problem.
You're likely calling Mailer.notification_to_sender.deliver in your controller, or better yet, a background job. I'm guessing notification_to_sender probably takes a parameter as well.
Anyways, when you call the notification_to_sender method on Mailer you're getting back an instance of Mail::Message that has the deliver method on it. If you were simply doing Mailer.notification_to_sender without also calling deliver, you could run what you have there with the comments and all would be fine. I would guess you're also calling deliver though.
In that case your failure message would be something like
NoMethodError:
undefined method `deliver' for nil:NilClass
That is because nil is Ruby's default return value much of the time, which Rails also inherits. Without specifying the mailer = mock and .and_return(mailer) parts, when the controller executes in context of the test then notification_to_sender will return nil and the controller will try to call deliver on that nil object.
The solution you have commented out is to mock out notification_to_sender's return value (normally Mail::Message) and then expect that deliver method to be called on it.