Warning: Creating default object from empty value in /homepages/39/d161420129/htdocs/p373.net/wp/wp-content/themes/p373b/admin/functions.php on line 183
Stub a ruby method and conditionally raise an exception based upon state of object

Stub a ruby method and conditionally raise an exception based upon state of object

(If you want to skip my fancy and hilarious writing, skip straight to the SO post)

Here’s the use case:

You’re building  a mass email system for your globo-mega-corp’s 90,000 employees.  Your corp’s smtp server can only handle batches of 450 recipients at a time.  Your boss tells you your corp’s SMTP server is finicky and rejects batches from time to time for no apparent reason.  She also dictates that if one batch fails the rest must succeed and you must keep track of which recipients received the email and which did not so that you can resend the batches that failed.  Easy enough, you think, we’re using Ruby on Rails.  And you write something like the following code:

def send_batches(batches)
  batches.each do |batch|
    begin
      batch.deliver!
    rescue Exception => e
      batch.update_attributes(failed_at: Time.now, failure: e.message)
    end
  end

  if batches.failed.length > 0
    ExceptionNotifier.send("Some batches failed")
  else
    batches.destory_all
  end
end

Its not the best code, but you’re under deadline because the CEO of GloboMegaCorp wants to invite everyone over to his mansion for daiquiris this weekend.   So now, like the good little developer you are, you write your test code(actually, you wrote your test code first, didn’t you??).

 

it "sends multiple batches gracefully when one fails in the middle" do

  mm = MassMailer.new(subject: "Daiquiris, Lets do it!", from: "Don Juan The Ceo", to: "GloboMegaCorp Staff List", body: "My house on Sat, bring your speed-o!")
  mm.send_batches(Staff.all.each_slice(450).to_a)

end

Did you hear the record scratch in your brain?  How are you going to test that your exception handling code works?  You need to trigger an exception on one of the batches but let the rest proceed as normal….hmmm….do you hear the Jeopardy music playing?? Times up!

Here’s how we can make one object that we don’t have control over throw an exception in our test:

Mail::Message.any_instance.stub(:deliver) do |message|
  raise Exception, "oh no you di'nt" if message.bcc and message.bcc.include?("vanna.white@globomegacorp.com")
  ActionMailer::Base.deliveries << message
end

So what I’ve done here is stubbed out the deliver call on any instance of Mail::Message and I throw an exception at my discretion.  In this case, I throw an exception on the batch that Vanna is in.  I also make sure to add in the message to the deliveries array so I can test actual deliveries, particularly the delivery of the exception notification.  Otherwise, mails do not get “sent” because we’ve stubbed out the “deliver” method, remember?

Now, dear reader, you might have skipped straight to the code and tried it out, and screamed some profanities at your dear author that this code is buggy.  Well, perhaps you should keep reading then and not be so hasty, ey?

This code will work for Rspec version 3.  What’s that?  You’re still on v2?  Oh, well so am I.  I even tried upgrading, but that was a nightmare this little old legacy app that contrived this sweet example.  So lucky for you I’ve got the tonic:

class RSpec::Mocks::MessageExpectation
  def invoke_with_orig_object(parent_stub, *args, &block)
    raise "Delete me.  I was just stubbed to pull in behavior from RSpec v3 before it was production ready to fix a bug!  But now I see you are using Rspec v3. See this commit: https://github.com/rspec/rspec-mocks/commit/ebd1cdae3eed620bd9d9ab08282581ebc2248535#diff-060466b2a68739ac2a2798a9b2e78643" if RSpec::Version::STRING > "2.99.0.pre"
    args.unshift(@method_double.object)
    invoke_without_orig_object(parent_stub, *args, &block)
  end
  alias_method_chain :invoke, :orig_object
end

Drop that bad boy at the bottom of your test and you’re good to go.  You’ll notice I was even nice enough to throw an exception to warn you when you’ve upgraded.  Because if you’re like me, I will upgrade months from now and have no recollection that I did this nifty monkey patch.

Happy days.

    This entry was posted in Coding, Ruby/Rails, Technology and tagged , , . Bookmark the permalink. Both comments and trackbacks are currently closed.