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
1 Answer 1
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 :)
-
\$\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– Damien Wilson2014年11月20日 00:19:58 +00:00Commented Nov 20, 2014 at 0:19
after_create
is called, well, after the record's been persisted. So a failure in the notification mailer shouldn't affect it. \$\endgroup\$User.first.teams << Team.first
and thatafter_create
action raised an error, theTeamMembership
record does not seem to get persisted. \$\endgroup\$test_notifier.verify
without the assert) \$\endgroup\$