I am new with Ruby/Rails and the testing frameworks within Ruby/Rails. I have a pre-validation method (external API) that validates the incoming request. For all test cases, I want to stub that call and test the remaining functionalities.
I am knowledgeable about testing and mocks/stubs/spies (mostly Mockito/Powermockito stuffs), but do not know my way around Rails testing. I tried looking into RSpec / MiniTest stuffs, but it is getting overwhelming.
I have a controller method like this:
def handler
# validate
request_validated = validate_request
unless request_validated
head :unauthorized
return
end
#... remaining codes
end
def validate_request
# validation with external API
end
I have controller tests set up using ActionController::TestCase. Prior to adding the validation stuffs, all my test cases have tested out. But I cannot stub around the validation check.
I would want to have something like
controller.stub(validate_request).then_and_return(true) # need something of this sort
post :handler, as: :json, params: completed_service_parameters
assert_response :no_content
I'm open to using any library, though would prefer to use any Rails in-built, if there's anything. Thanks.
I ended up using 'minitest/stub_any_instance'
require 'minitest/stub_any_instance'
...
test 'test stub' do
...
Controller.stub_any_instance(:function, return-value) do
# perform the call within the stubbed block
post :function, as: :json, params: { :param1 => 'random' }
end
...
end
Please bear with me while I give some background to my question:
I was recently integrating CanCan into our application and found that one of the controller rspec tests failed. Turns out this was a result of the test being poorly written and invoking an action on the controller twice.
it 'only assigns users for the organisation as #users' do
xhr :get, :users, { id: first_organisation.to_param}
expect(assigns(:users).count).to eq(3)
xhr :get, :users, { id: second_organisation.to_param}
expect(assigns(:users).count).to eq(4)
end
Note, the example is cut for brevity.
Now the reason this fails is because rspec is using the same controller instance for both action calls and CanCan only loads the organisation resource if it isn't already loaded.
I can accept the reasoning behind a) rspec using a single instance of the controller for the scope of the example and b) for CanCan to be only loading the resource if it doesn't exist.
The real issue here is that of course it's a bad idea to be invoking an action twice within the same example. Now the introduction of CanCan highlighted the error in this example, but I am now concerned there may be other controller tests which are also invoking actions twice or that such examples may be written in the future, which, rather long-windedly, leads me to my question:
Is it possible to enforce that a controller rspec example can only invoke a single controller action?
Ok, it does appear I have a solution:
Create unity_helper.rb to go into spec/support
module UnityExtension
class UnityException < StandardError
end
def check_unity
if #unity
raise UnityException, message: 'Example is not a unit test!'
end
#unity = true
end
end
RSpec.configure do |config|
config.before(:all, type: :controller) do
ApplicationController.prepend UnityExtension
ApplicationController.prepend_before_action :check_unity
end
end
then in rails_helper.rb
require 'support/unity_helper'
And this has actually highlighted another rspec controller example which is invoking a controller twice.
I am open to other solutions or improvements to mine.
I am testing a controller and would like to force a db update to fail in order to verify that my error handling is working properly. I am fairly new to rails so I apologize if I am not following all of the best practices. Below is the relevant code:
Code is not complete as to focus on the important parts relevant to this question.
Controller:
class SomeController < ApplicationController
...
# relevant actions
def enable
able true
end
def disable
able false
end
...
private
def able (b)
...
# #dbobject will be set in a 'before_filter' function
if #dbobject.update_attribute(enabled: b)
# do some stuff
else # <------ need to force execution of this block
# error handling, logging, boring stuff
redirect_to #dbobject
...
end
...
end
Test:
class SomeController::AbleTest < ActionController::TestCase
tests SomeController
setup
# create #dbobject
end
test 'redirect occurs on update fail' do
get :enable, id: #dbobject
assert_redirected_to #dbobject
end
...
end
I also have tests in SomeController::AbleTest that require update_attribute to work properly so I would prefer to stay away from overriding the method completely. Is there any way to force the db to raise an exception if this record is accessed or something similar? I am not able to call the able method directly from the test class because it relies heavily on instance variables set by various before_filter methods and it feels too much like fighting the framework to not make the get :enable and have these methods run automatically.
You can use stubs. Look into mocha:
DBObject.any_instance.stubs(:update_attribute).returns(false)
This would mean whenever you can update_attribute on any instance of DBObject, it would return false, sending you into the else. So teh whole code would be:
test 'redirect occurs on update fail' do
DBObject.any_instance.stubs(:update_attribute).returns(false)
get :enable, id: #dbobject
assert_redirected_to #dbobject
end
Try using the mocha gem. You can temporarily stub update_attributes so it returns false.
dbobject.stubs(:update_attributes).returns(false)
Note that dbobject isn't the same as the #dbobject you're passing to get in your test. You'll have to stub the code that fetches the record in your controller. This is more troublesome than Yule's way of any_instance, so try that first.
trying to upgrade to Rails 4.2, using delayed_job_active_record. I've not set the delayed_job backend for test environment as thought that way jobs would execute straight away.
I'm trying to test the new 'deliver_later' method with RSpec, but I'm not sure how.
Old controller code:
ServiceMailer.delay.new_user(#user)
New controller code:
ServiceMailer.new_user(#user).deliver_later
I USED to test it like so:
expect(ServiceMailer).to receive(:new_user).with(#user).and_return(double("mailer", :deliver => true))
Now I get errors using that. (Double "mailer" received unexpected message :deliver_later with (no args))
Just
expect(ServiceMailer).to receive(:new_user)
fails too with 'undefined method `deliver_later' for nil:NilClass'
I've tried some examples that allow you to see if jobs are enqueued using test_helper in ActiveJob but I haven't managed to test that the correct job is queued.
expect(enqueued_jobs.size).to eq(1)
This passes if the test_helper is included, but it doesn't allow me to check it is the correct email that is being sent.
What I want to do is:
test that the correct email is queued (or executed straight away in test env)
with the correct parameters (#user)
Any ideas??
thanks
If I understand you correctly, you could do:
message_delivery = instance_double(ActionMailer::MessageDelivery)
expect(ServiceMailer).to receive(:new_user).with(#user).and_return(message_delivery)
allow(message_delivery).to receive(:deliver_later)
The key thing is that you need to somehow provide a double for deliver_later.
Using ActiveJob and rspec-rails 3.4+, you could use have_enqueued_job like this:
expect {
YourMailer.your_method.deliver_later
# or any other method that eventually would trigger mail enqueuing
}.to(
have_enqueued_job.on_queue('mailers').with(
# `with` isn't mandatory, but it will help if you want to make sure is
# the correct enqueued mail.
'YourMailer', 'your_method', 'deliver_now', any_param_you_want_to_check
)
)
also double check in config/environments/test.rb you have:
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
Another option would be to run inline jobs:
config.active_job.queue_adapter = :inline
But keep in mind this would affect the overall performance of your test suite, as all your jobs will run as soon as they're enqueued.
If you find this question but are using ActiveJob rather than simply DelayedJob on its own, and are using Rails 5, I recommend configuring ActionMailer in config/environments/test.rb:
config.active_job.queue_adapter = :inline
(this was the default behavior prior to Rails 5)
I will add my answer because none of the others was good enough for me:
1) There is no need to mock the Mailer: Rails basically does that already for you.
2) There is no need to really trigger the creation of the email: this will consume time and slow down your test!
That's why in environments/test.rb you should have the following options set:
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
Again: don't deliver your emails using deliver_now but always use deliver_later. That prevents your users from waiting for the effective delivering of the email. If you don't have sidekiq, sucker_punch, or any other in production, simply use config.active_job.queue_adapter = :async. And either async or inline for development environment.
Given the following configuration for the testing environment, you emails will always be enqueued and never executed for delivery: this prevents your from mocking them and you can check that they are enqueued correctly.
In you tests, always split the test in two:
1) One unit test to check that the email is enqueued correctly and with the correct parameters
2) One unit test for the mail to check that the subject, sender, receiver and content are correct.
Given the following scenario:
class User
after_update :send_email
def send_email
ReportMailer.update_mail(id).deliver_later
end
end
Write a test to check the email is enqueued correctly:
include ActiveJob::TestHelper
expect { user.update(name: 'Hello') }.to have_enqueued_job(ActionMailer::DeliveryJob).with('ReportMailer', 'update_mail', 'deliver_now', user.id)
and write a separate test for your email
Rspec.describe ReportMailer do
describe '#update_email' do
subject(:mailer) { described_class.update_email(user.id) }
it { expect(mailer.subject).to eq 'whatever' }
...
end
end
You have tested exactly that your email has been enqueued and not a generic job.
Your test is fast
You needed no mocking
When you write a system test, feel free to decide if you want to really deliver emails there, since speed doesn't matter that much anymore. I personally like to configure the following:
RSpec.configure do |config|
config.around(:each, :mailer) do |example|
perform_enqueued_jobs do
example.run
end
end
end
and assign the :mailer attribute to the tests were I want to actually send emails.
For more about how to correctly configure your email in Rails read this article: https://medium.com/#coorasse/the-correct-emails-configuration-in-rails-c1d8418c0bfd
Add this:
# spec/support/message_delivery.rb
class ActionMailer::MessageDelivery
def deliver_later
deliver_now
end
end
Reference: http://mrlab.sk/testing-email-delivery-with-deliver-later.html
A nicer solution (than monkeypatching deliver_later) is:
require 'spec_helper'
include ActiveJob::TestHelper
describe YourObject do
around { |example| perform_enqueued_jobs(&example) }
it "sends an email" do
expect { something_that.sends_an_email }.to change(ActionMailer::Base.deliveries, :length)
end
end
The around { |example| perform_enqueued_jobs(&example) } ensures that background tasks are run before checking the test values.
I came with the same doubt and resolved in a less verbose (single line) way inspired by this answer
expect(ServiceMailer).to receive_message_chain(:new_user, :deliver_later).with(#user).with(no_args)
Note that the last with(no_args) is essential.
But, if you don't bother if deliver_later is being called, just do:
expect(ServiceMailer).to expect(:new_user).with(#user).and_call_original
A simple way is:
expect(ServiceMailer).to(
receive(:new_user).with(#user).and_call_original
)
# subject
This answer is for Rails Test, not for rspec...
If you are using delivery_later like this:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
…
def create
…
# Yes, Ruby 2.0+ keyword arguments are preferred
UserMailer.welcome_email(user: #user).deliver_later
end
end
You can check in your test if the email has been added to the queue:
# test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
…
test 'email is enqueued to be delivered later' do
assert_enqueued_jobs 1 do
post :create, {…}
end
end
end
If you do this though, you’ll surprised by the failing test that tells you assert_enqueued_jobs is not defined for us to use.
This is because our test inherits from ActionController::TestCase which, at the time of writing, does not include ActiveJob::TestHelper.
But we can quickly fix this:
# test/test_helper.rb
class ActionController::TestCase
include ActiveJob::TestHelper
…
end
Reference:
https://www.engineyard.com/blog/testing-async-emails-rails-42
For recent Googlers:
allow(YourMailer).to receive(:mailer_method).and_call_original
expect(YourMailer).to have_received(:mailer_method)
I think one of the better ways to test this is to check the status of job alongside the basic response json checks like:
expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.on_queue('mailers').with('mailer_name', 'mailer_method', 'delivery_now', { :params => {}, :args=>[] } )
I have come here looking for an answer for a complete testing, so, not just asking if there is one mail waiting to be sent, in addition, for its recipient, subject...etc
I have a solution, than comes from here, but with a little change:
As it says, the curial part is
mail = perform_enqueued_jobs { ActionMailer::DeliveryJob.perform_now(*enqueued_jobs.first[:args]) }
The problem is that the parameters than mailer receives, in this case, is different from the parameters than receives in production, in production, if the first parameter is a Model, now in testing will receive a hash, so will crash
enqueued_jobs.first[:args]
["UserMailer", "welcome_email", "deliver_now", {"_aj_globalid"=>"gid://forjartistica/User/1"}]
So, if we call the mailer as UserMailer.welcome_email(#user).deliver_later the mailer receives in production a User, but in testing will receive {"_aj_globalid"=>"gid://forjartistica/User/1"}
All comments will be appreciate,
The less painful solution I have found is changing the way that I call the mailers, passing, the model's id and not the model:
UserMailer.welcome_email(#user.id).deliver_later
This answer is a little bit different, but may help in cases like a new change in the rails API, or a change in the way you want to deliver (like use deliver_now instead of deliver_later).
What I do most of the time is to pass a mailer as a dependency to the method that I am testing, but I don't pass an mailer from rails, I instead pass an object that will do the the things in the "way that I want"...
For example if I want to check that I am sending the right mail after the registration of a user... I could do...
class DummyMailer
def self.send_welcome_message(user)
end
end
it "sends a welcome email" do
allow(store).to receive(:create).and_return(user)
expect(mailer).to receive(:send_welcome_message).with(user)
register_user(params, store, mailer)
end
And then in the controller where I will be calling that method, I would write the "real" implementation of that mailer...
class RegistrationsController < ApplicationController
def create
Registrations.register_user(params[:user], User, Mailer)
# ...
end
class Mailer
def self.send_welcome_message(user)
ServiceMailer.new_user(user).deliver_later
end
end
end
In this way I feel that I am testing that I am sending the right message, to the right object, with the right data (arguments). And I am just in need of creating a very simple object that has no logic, just the responsibility of knowing how ActionMailer wants to be called.
I prefer to do this because I prefer to have more control over the dependencies I have. This is form me an example of the "Dependency inversion principle".
I am not sure if it is your taste, but is another way to solve the problem =).
Having been inspired by Sandi Metz's approach to writing tests (http://www.confreaks.com/videos/2452-railsconf2013-the-magic-tricks-of-testing), I am trying to refactor a test for a Rails controller to assert that it is sending a command message properly.
Here are the relevant parts of the Application:
class DealsController < ApplicationController
def index
if params[:reset]
deal_filter.reset
...
class ApplicationController
def deal_filter
...
#deal_filter ||= DealFilter.new(args)
end
...
class DealFilter
def reset
...do work...
end
...
And here is the rspec test:
describe DealsController do
it "should send 'reset' to the deal_filter" do
df = instance_double("DealFilter")
get :index, reset: "true"
expect(df).to receive(:reset)
end
end
The test results that keep coming back are:
1) DealsController GET index for any user params contain 'reset' should send 'reset' to the deal_filter
Failure/Error: expect(df).to receive(:reset)
(Double "DealFilter (instance)").reset(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
I have already confirmed that the reset param is being sent through the test and that the controller is following the appropriate path, yet the test continues to fail.
Can anyone suggest a possible reason for the failure or resources for further study? I am relatively new to object oriented thinking and using mocks with Rspec. Could it be that I have misunderstood the role of doubles?
Thanks for your time!
You need to make sure your double gets used. I think the best way to do that here is to stub the deal_filter method to return the double.
I addition I would isolate the expection, so that it's the only thing in the it block. This will make it easier to add more expections without duplication the setup logic.
describe DealsController do
let(:df) { instance_double("DealFilter") }
before do
allow(controller).to receive(:deal_filter).and_return(df)
get :index, reset: "true"
end
it "should send 'reset' to the deal_filter" do
expect(df).to have_received(:reset)
end
end
I think you're expecting your instance_double to be used automatically somewhere within the index action. That's not how doubles work. You can create a double and use it for things, but your code in the controller doesn't (and shouldn't) know anything about that double and so won't ever call anything on it.
For an example of how an instance double can actually be used see this documentation.
Another issue with your expectation is that you're not setting it early enough. When you expect an object to receive a method call there needs to be something that happens after that which would invoke that method. In your example the expectation to receive :reset is the very last line of your example.
I'd recommend reading up on how other people have tested controllers with rspec as a good starting place.