Mikel Lindsaar
2007-Nov-04 09:55 UTC
[rspec-users] Specing raising error, handling, and then not raising error
Hey guys and gals, I have a snippet of code: Net::SMTP(@host, @port, @from_domain) do |smtp| @emails.each do |email| begin smtp.send_message email.encoded, email.from, email.destinations @emails_sent += 1 rescue Exception => e # blah end end end What I want to do is: Say there are 4 emails. First email is sent OK On the second email smtp raises a IOError error The third and fourth emails are sent OK I want to spec: That the second iteration raises an error. That the total emails sent count changed by 3 I can''t seem to figure out how to stub/mock the smtp object to return an email on the 1, 3, and 4th iteration and raise an error on the 2nd iteration. I thought something like this, but it doesn''t work: it "should only increase the sent emails counter if the email was sent" do @smtp = mock(Net::SMTP) @smtp.should_receive(:send_message).exactly(4).times.and_return(true, IOError, true, true) Net::SMTP.stub!(:start).and_yield(@smtp) @sender = Mailer::Sender.new(valid_args(:emails => @emails)) @sender.sent_emails.should == 3 end If anyone has any pointers, that would be great. I am using rSpec trunk - today''s release. Mikel
Ashley Moran
2007-Nov-04 15:20 UTC
[rspec-users] Specing raising error, handling, and then not raising error
On Nov 04, 2007, at 9:55 am, Mikel Lindsaar wrote:> it "should only increase the sent emails counter if the email was > sent" do > @smtp = mock(Net::SMTP) > > @smtp > .should_receive(:send_message).exactly(4).times.and_return(true, > IOError, true, true) > Net::SMTP.stub!(:start).and_yield(@smtp) > @sender = Mailer::Sender.new(valid_args(:emails => @emails)) > @sender.sent_emails.should == 3 > endMikel, It looks like you are doing too much here. You are specifying sending with the SMTP object in the same block you are specifying the algorithm for counting the sent emails. Also, the way it interrogates the email object to extract data looks fragile. I would prefer to make the Email object responsible for sending itself, although I will await the approval of an expert before I tell you my solution is better! I had a go at this, and my solution is MUCH longer, but it breaks the behaviour down to a finer level. (As you didn''t show the code for Mailer::Sender, I wrote a quick implementation myself, possibly similar to yours.) Also, I''m never quite sure how best to use "before" blocks, so don''t take mine as a canonical example. Anyway here is what I came up with: class Email def initialize(encoded_message, from, destination) @encoded_message, @from, @destination encoded_message, from, destination end def send_via(smtp) begin smtp.send_message(@encoded_message, @from, @destination) rescue IOError => e 0 else 1 end end end describe Email, :shared => true do before(:each) do @smtp = mock("Net::SMTP") @email = Email.new("message", "from at mydomain", "to at destination") end it "send_via: should send :send_message to the SMTP object with the email details" do @smtp.should_receive(:send_message). with("message", "from at mydomain", "to at destination") @email.send_via(@smtp) end end describe Email, "sent via a working SMTP" do it_should_behave_like "Email" before(:each) do @smtp.stub!(:send_message) end it "send_via: should return 1 if the email sent successfully" do @email.send_via(@smtp).should == 1 end end describe Email, "sent via a faulty SMTP" do it_should_behave_like "Email" before(:each) do @smtp.stub!(:send_message).and_raise(IOError.new) end it "send_via: should return 1 if the email sent successfully" do @email.send_via(@smtp).should == 0 end end class Spammer def initialize(smtp) @smtp = smtp end def send(emails) emails.inject(0) { |success_count, email| success_count + email.send_via(@smtp) } end end describe Spammer, "created with an SMTP" do before(:each) do @email_successful_1 = mock(Email) @email_successful_1.stub!(:send_via).and_return(1) @email_successful_2 = mock(Email) @email_successful_2.stub!(:send_via).and_return(1) @email_unsuccessful_1 = mock(Email) @email_unsuccessful_1.stub!(:send_via).and_return(0) @emails = [ @email_successful_1, @email_successful_2, @email_unsuccessful_1] @spammer = Spammer.new(:smtp) end it "send: should send each email in turn with the SMTP and return the successful count" do @email_successful_1.should_receive(:send_via).with(:smtp).and_return(1) @email_successful_2.should_receive(:send_via).with(:smtp).and_return(1) @email_unsuccessful_1 .should_receive(:send_via).with(:smtp).and_return(2) @spammer.send(@emails) end it "send: should return the count of successful emails" do @spammer.send(@emails).should == 2 end end Let me know if this is helpful Regards Ashley -- blog @ http://aviewfromafar.net/ linked-in @ http://www.linkedin.com/in/ashleymoran currently @ home
Mikel Lindsaar
2007-Nov-04 23:51 UTC
[rspec-users] Specing raising error, handling, and then not raising error
On 11/5/07, Ashley Moran <work at ashleymoran.me.uk> wrote:> Mikel, > > It looks like you are doing too much here. You are specifying sending > with the SMTP object in the same block you are specifying the > algorithm for counting the sent emails. Also, the way it interrogates > the email object to extract data looks fragile. I would prefer to > make the Email object responsible for sending itself, although I will > await the approval of an expert before I tell you my solution is better!HOLYCOMPLETESOLUTIONBATMAN! :) Definately helpful... especially the bit on "trying to do too much". The problem I was trying to solve with the mass sender is I only want to open one connection to the SMTP server, not multiple. But your code definatley gave me some good ideas. All this to get some email from one computer on the internet to another that is not :) Anyway, speak soon, thanks again! Mikel
Ashley Moran
2007-Nov-05 15:53 UTC
[rspec-users] Specing raising error, handling, and then not raising error
On 4 Nov 2007, at 23:51, Mikel Lindsaar wrote:> The problem I was trying to solve with the mass sender is I only want > to open one connection to the SMTP server, not multiple.I thought I covered that. I didn''t mock out the bit that wrapped your existing code: Net::SMTP(@host, @port, @from_domain) do |smtp| # ... previous implementation end So at some point you still need that one connection that gets passed through the rest of the code. Class Spammer holds onto the SMTP but it''s a transient object really - I pictured it being made in the Net::SMTP block and discarded, eg: Net::SMTP(@host, @port, @from_domain) do |smtp| Spammer.new(smtp).send(@emails) end Does this help? Ashley -- blog @ http://aviewfromafar.net/ linked-in @ http://www.linkedin.com/in/ashleymoran currently @ work