Browse files

Merge pull request #264 from phildarnowsky/master

Shoulda-style RSpec matchers
  • Loading branch information...
2 parents 7afba28 + 282bffb commit bf2c605c7496c894c657191f4cd9276dd6e4eb40 @mikel mikel committed Jan 13, 2012
Showing with 304 additions and 0 deletions.
  1. +49 −0 README.mkd
  2. +2 −0 lib/mail.rb
  3. +124 −0 lib/mail/matchers/has_sent_mail.rb
  4. +129 −0 spec/matchers_spec.rb
View
49 README.mkd
@@ -537,6 +537,55 @@ Mail::TestMailer.deliveries.clear
=> []
```
+There is also a set of RSpec matchers stolen fr^H^H^H^H^H^H^H^H inspired by Shoulda's ActionMailer matchers (you'll want to set <code>delivery_method</code> as above too):
+
+```
+Mail.defaults do
+ delivery_method :test # in practice you'd do this in spec_helper.rb
+end
+
+describe "sending an email" do
+ include Mail::Matchers
+
+ before(:each) do
+ Mail::TestMailer.deliveries.clear
+
+ Mail.deliver do
+ to ['mikel@me.com', 'mike2@me.com']
+ from 'you@you.com'
+ subject 'testing'
+ body 'hello'
+ end
+ end
+
+ it { should have_sent_email } # passes if any email at all was sent
+
+ it { should have_sent_email.from('you@you.com') }
+ it { should have_sent_email.to('mike1@me.com') }
+
+ # can specify a list of recipients...
+ it { should have_sent_email.to(['mike1@me.com', 'mike2@me.com']) }
+
+ # ...or chain recipients together
+ it { should have_sent_email.to('mike1@me.com').to('mike2@me.com') }
+
+ it { should have_sent_email.with_subject('testing') }
+
+ it { should have_sent_email.with_body('hello') }
+
+ # Can match subject or body with a regex
+ # (or anything that responds_to? :match)
+
+ it { should have_sent_email.matching_subject(/test(ing)?/) }
+ it { should have_sent_email.matching_body(/h(a|e)llo/) }
+
+ # Can chain together modifiers
+ # Note that apart from recipients, repeating a modifier overwrites old value.
+
+ it { should have_sent_email.from('you@you.com').to('mike1@me.com').matching_body(/hell/)
+end
+```
+
Excerpts from TREC Spam Corpus 2005
-----------------------------------
View
2 lib/mail.rb
@@ -84,6 +84,8 @@ module Mail # :doc:
require 'mail/encodings/base64'
require 'mail/encodings/quoted_printable'
+ require 'mail/matchers/has_sent_mail'
+
# Finally... require all the Mail.methods
require 'mail/mail'
end
View
124 lib/mail/matchers/has_sent_mail.rb
@@ -0,0 +1,124 @@
+module Mail
+ module Matchers
+ def have_sent_email
+ HasSentEmailMatcher.new(self)
+ end
+
+ class HasSentEmailMatcher
+ def initialize(_context)
+ end
+
+ def matches?(subject)
+ matching_deliveries = filter_matched_deliveries(Mail::TestMailer.deliveries)
+ !(matching_deliveries.empty?)
+ end
+
+ def from(sender)
+ @sender = sender
+ self
+ end
+
+ def to(recipient_or_list)
+ @recipients ||= []
+
+ if recipient_or_list.kind_of?(Array)
+ @recipients += recipient_or_list
+ else
+ @recipients << recipient_or_list
+ end
+ self
+ end
+
+ def with_subject(subject)
+ @subject = subject
+ self
+ end
+
+ def matching_subject(subject_matcher)
+ @subject_matcher = subject_matcher
+ self
+ end
+
+ def with_body(body)
+ @body = body
+ self
+ end
+
+ def matching_body(body_matcher)
+ @body_matcher = body_matcher
+ self
+ end
+
+ def description
+ result = "send a matching email"
+ result
+ end
+
+ def failure_message
+ result = "Expected email to be sent "
+ result += explain_expectations
+ result += dump_deliveries
+ result
+ end
+
+ def negative_failure_message
+ result = "Expected no email to be sent "
+ result += explain_expectations
+ result += dump_deliveries
+ result
+ end
+
+ protected
+
+ def filter_matched_deliveries(deliveries)
+ candidate_deliveries = deliveries
+
+ %w(sender recipients subject subject_matcher body body_matcher).each do |modifier_name|
+ next unless instance_variable_defined?("@#{modifier_name}")
+ candidate_deliveries = candidate_deliveries.select{|matching_delivery| self.send("matches_on_#{modifier_name}?", matching_delivery)}
+ end
+
+ candidate_deliveries
+ end
+
+ def matches_on_sender?(delivery)
+ delivery.from.include?(@sender)
+ end
+
+ def matches_on_recipients?(delivery)
+ @recipients.all? {|recipient| delivery.to.include?(recipient) }
+ end
+
+ def matches_on_subject?(delivery)
+ delivery.subject == @subject
+ end
+
+ def matches_on_subject_matcher?(delivery)
+ @subject_matcher.match delivery.subject
+ end
+
+ def matches_on_body?(delivery)
+ delivery.body == @body
+ end
+
+ def matches_on_body_matcher?(delivery)
+ @body_matcher.match delivery.body.raw_source
+ end
+
+ def explain_expectations
+ result = ''
+ result += "from #{@sender} " if instance_variable_defined?('@sender')
+ result += "to #{@recipients.inspect} " if instance_variable_defined?('@recipients')
+ result += "with subject \"#{@subject}\" " if instance_variable_defined?('@subject')
+ result += "with subject matching \"#{@subject_matcher}\" " if instance_variable_defined?('@subject_matcher')
+ result += "with body \"#{@body}\" " if instance_variable_defined?('@body')
+ result += "with body matching \"#{@body_matcher}\" " if instance_variable_defined?('@body_matcher')
+ result
+ end
+
+ def dump_deliveries
+ "(actual deliveries: " + Mail::TestMailer.deliveries.inspect + ")"
+ end
+ end
+ end
+end
View
129 spec/matchers_spec.rb
@@ -0,0 +1,129 @@
+require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper')
+
+describe "have_sent_email" do
+ include Mail::Matchers
+
+ def send_test_email
+ Mail.deliver do
+ from 'phil@example.com'
+ to ['bob@example.com', 'fred@example.com']
+ subject 'The facts you requested'
+ body 'Here are the facts you requested. One-onethousand, two-onethousand.'
+ end
+ end
+
+ before(:all) do
+ $old_delivery_method = Mail.delivery_method
+
+ Mail.defaults do
+ delivery_method :test
+ end
+ end
+
+ after(:all) do
+ # Although this breaks encapsulation, it's the easiest way to ensure
+ # that the delivery method is _exactly_ what it was before we started
+ # messing with it.
+
+ Mail::Configuration.instance.instance_variable_set(:@delivery_method, $old_delivery_method)
+ end
+
+ context "without any modifiers" do
+ context "when no e-mail has been sent" do
+ before(:each) do
+ Mail::TestMailer.deliveries.should be_empty
+ end
+
+ it { should_not have_sent_email }
+ end
+
+ context "when e-mail has been sent" do
+ before(:each) do
+ send_test_email
+ Mail::TestMailer.deliveries.should_not be_empty
+ end
+
+ it { should have_sent_email }
+ end
+ end
+
+ context "with #from" do
+ context "and a matching sender" do
+ it { should have_sent_email.from('phil@example.com') }
+ end
+
+ context "and a non-matching sender" do
+ it { should_not have_sent_email.from('sven@example.com') }
+ end
+ end
+
+ context "with #to" do
+ context "and a matching recipient" do
+ it { should have_sent_email.to('bob@example.com') }
+ it { should have_sent_email.to('fred@example.com') }
+ it { should have_sent_email.to('bob@example.com').to('fred@example.com') }
+ it { should have_sent_email.to(['bob@example.com', 'fred@example.com']) }
+ end
+
+ context "and a non-matching recipient" do
+ it { should_not have_sent_email.to('sven@example.com') }
+ end
+ end
+
+ context "with #subject" do
+ context "and a matching subject" do
+ it { should have_sent_email.with_subject('The facts you requested') }
+ end
+
+ context "and a non-matching subject" do
+ it { should_not have_sent_email.with_subject('facts you requested') }
+ it { should_not have_sent_email.with_subject('the facts you') }
+ it { should_not have_sent_email.with_subject('outright lies') }
+ end
+ end
+
+ context "with #subject_matching" do
+ context "and a matching subject" do
+ it { should have_sent_email.matching_subject(/(facts|fiction) you requested/) }
+ end
+
+ context "and a non-matching subject" do
+ it { should_not have_sent_email.matching_subject(/The \d+ facts you requested/) }
+ end
+ end
+
+ context "with #with_body" do
+ context "and a matching body" do
+ it { should have_sent_email.with_body('Here are the facts you requested. One-onethousand, two-onethousand.') }
+ end
+
+ context "and a non-matching body" do
+ it { should_not have_sent_email.with_body('Here are the facts you requested.') }
+ it { should_not have_sent_email.with_body('are the facts you requested. One-onethousand') }
+ it { should_not have_sent_email.with_body('Be kind to your web-footed friends, for a duck may be somebody\'s mother') }
+ end
+ end
+
+ context "with #matching_body" do
+ context "and a matching body" do
+ it { should have_sent_email.matching_body(/one-?one(hundred|thousand)/i) }
+ end
+
+ context "and a non-matching body" do
+ it { should_not have_sent_email.matching_body(/\d+-onethousand/) }
+ end
+ end
+
+ context "with a huge chain of modifiers" do
+ it do
+ should have_sent_email.
+ from('phil@example.com').
+ to('bob@example.com').
+ to('fred@example.com').
+ with_subject('The facts you requested').
+ matching_subject(/facts (I|you)/).
+ with_body('Here are the facts you requested. One-onethousand, two-onethousand.').
+ matching_body(/(I|you) request/)
+ end
+ end
+end

0 comments on commit bf2c605

Please sign in to comment.