1
\$\begingroup\$

I've recently started working on a new application in Rails 4.2.0-beta2. I've been using this as an opportunity to learn more about MiniTest, with a stretch goal of being as strict with myself with testing as possible.

My general problem: How do I test the after_save action for an ActiveRecord model in such a way that I can be confident that an ActiveRecord callback will not cause my code to break?

Here's what I've got so far (gist or inline code below).

How would you (re)write these files to detect if the NotificationMailer called in the after_save hook prevented a TeamMembership from being persisted?

Note that not included here are the relevant test fixtures. You may safely assume that fixtures users(:carol) and teams(:alpha) are not associated with each other.

app/models/team_membership.rb

class TeamMembership < ActiveRecord::Base
  # A proc that will enqueue `NotificationMailer.team_invitation`
  DEFAULT_NOTIFIER = proc do |user, team|
    NotificationMailer.team_invitation(team, user).deliver_later
  end

  class << self
    # This is a class level attribute that is mainly used for testing.
    # Defaults to {TeamMembership::DEFAULT_NOTIFIER}
    attr_accessor :notifier
  end
  self.notifier = DEFAULT_NOTIFIER

  belongs_to :team
  belongs_to :user

  after_create :invite_user_to_team!

  private

  # ActiveRecord callback used to equeue team invitation emails
  # @return void
  def invite_user_to_team!
    self.class.notifier.call(team, user)
    nil
  end
end

tests/models/team_membership_test.rb

require 'test_helper'

class TeamMemberTest < ActiveSupport::TestCase
  test 'callbacks' do
    # setup
    test_notifier = Minitest::Mock.new
    test_notifier.expect(:call, nil, [teams(:alpha), users(:carol)])
    TeamMembership.notifier = test_notifier

    # test
    TeamMembership.create(user_id: users(:carol).id, team_id: teams(:alpha).id)
    assert test_notifier.verify

    # teardown
    TeamMembership.notifier = TeamMembership::DEFAULT_NOTIFIER
  end
end
\$\endgroup\$
  • \$\begingroup\$ Not sure I understand. after_create is called, well, after the record's been persisted. So a failure in the notification mailer shouldn't affect it. \$\endgroup\$ – Flambino Oct 13 '14 at 19:17
  • \$\begingroup\$ If you were to create the model directly, then yes, that is the case. Unfortunately if I were to do something like User.first.teams << Team.first and that after_create action raised an error, the TeamMembership record does not seem to get persisted. \$\endgroup\$ – Damien Wilson Oct 13 '14 at 19:24
  • \$\begingroup\$ Really? Hmm... I'll have to check that out when I have the time. (until then: I think you can just say test_notifier.verify without the assert) \$\endgroup\$ – Flambino Oct 13 '14 at 20:31
2
\$\begingroup\$

A valid approach would be check if NotificationMailer.team_invitation was called. In order to get this job done, you'll need first of all change this NotificationMailer.team_invitation hard-coded call to something injected. Something like this:

class TeamMembership < ActiveRecord::Base
  # other methods

  after_create :invite_user_to_team!

  def notification_method(notification_service = NotificationMailer)
    @notification = notification_service
  end

  private
  def invite_user_to_team!
    @notification.team_invitation(team, user)
    nil
  end
end

Now, you can create an expectation over NotificationMailer. Inside your test, you can define your mock and define its behavior:

class TeamMemberTest < ActiveSupport::TestCase
  def test_after_create_new_team_member_it_should_be_notified
    notification = Minitest::Mock.new
    notification.expects(:team_invitation).with(team, user).once
    # your TeamMember class working to create new
  end
end

I hope it helps :)

\$\endgroup\$
  • \$\begingroup\$ This is exactly the kind of feedback I was hoping for, thanks! I particularly like the way you default to the production use-case as a default method argument. This seems much cleaner and reads much more simply than the lambda constant I was using in my question. \$\endgroup\$ – Damien Wilson Nov 20 '14 at 0:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.