- Introduction
- Starting Test-Driven Development
- Always Look for New or Changed Requirements
- Sending Email Using Ruby
- Refactor Your Code To Make It Maintainable
Refactor Your Code To Make It Maintainable
The obvious things to fix immediately are the hard-coded mail server name and email address. We also need to break out sending the email from the code that finds the runners to send the email to.
Before doing this refactoring, check the original, untidy code into your source control systemjust to be on the safe side. After all, if you're shuffling a lot of code around there's always the chance that you might make a mistake.
require 'net/smtp' class Club attr_accessor :members def initialize(mail_server, club_email) @members = Array.new @email = club_email @smtp = Net::SMTP.new(mail_server) @smtp.start end def add (runner) members << runner end def send_notice (evt, str, &comparison) sent = 0 members.each {|r| selected = r.matches?(str, &comparison) if nil != selected then send_email(evt, selected ) sent += 1 end } return sent end def send_email(evt, r) @smtp.ready(@email, r.email) { |msg| msg.write "To: #{r.name} <#{r.email}>\r\n" msg.write "Subject: #{evt.name} on #{evt.date.to_s}\r\n" msg.write "\r\n" evt.description.each { |str| msg.write str + "\r\n" } } return true end endTC_Club.rb also needs a slight change to pass in the name of the mail server and the club's email address:
@club = Club.new("localhost", "secretary@club.ca")
Once the mainline code is working, you need to look at all of the weird and wonderful failures identified in the use case and acceptance test cases to make sure that the code will handle them correctly. To do that, the code needs to do some exception handling and separate selecting the runners from sending them email (otherwise we can't show how many members have been selected).
First we need to specify an extra test case for the new behaviorthat of selecting a set of runners:
def test_get_selected_members array = @club.get_selected_members("Pete") { |r,n| if n == r.name then r else nil end} assert_equal(1, array.size, "wrong number selected") endAnd then the Club class has to be refactored to extract the selection method:
class Club #... other parts omitted def get_selected_members (str, &comparison) ary = Array.new members.each {|r| selected = r.matches?(str, &comparison) if nil != selected then ary << selected end } return ary end def send_notice (evt, str, &comparison) sent = 0 runners = get_selected_members(str, &comparison) runners.each {|runner| if send_email(evt, runner) then sent += 1 end } return sent end #... other parts omitted end
This separation of selection from sending email allows the application to display the selected members before the emails are sent. Without the separation, the club secretary couldn't confirm that the right members have been selected.
The application also needs exception handling to deal with runtime errors that can be reported from the SMTP methods. If the mail server is unreachable in any way, or reports an error, the code will trap it so that it can be reported gracefully.
The rescue clause in the initialize method traps against things like unreachable mail servers. It simply prints the error message and clears out the smtp variable so that no further errors are reported. If the mail server is unreachable, you'll see a message something like this:
Errno::E10061 Unknown Error - \"connect(2)\"
The rescue clause in the send_email method traps error messages returned from the mail server. It simply prints the error message and returns false to indicate that the email was not sent. A possible error message back from the mail server is as follows:
Net::ProtoFatalError 550 550 5.7.1 Mail relay not allowed at this server
class Club attr_accessor :members def initialize(mail_server, club_email) @members = Array.new @email = club_email begin @smtp = Net::SMTP.new(mail_server) @smtp.start rescue Exception => err p err.class.to_s + " " + err.to_s @smtp = nil end end #... omitted parts def send_email(evt, r) if nil == @smtp then return false end begin @smtp.ready(@email, r.email) { |msg| msg.write "To: #{r.name} <#{r.email}>\r\n" msg.write "Subject: #{evt.name} on #{evt.date.to_s}\r\n" msg.write "\r\n" evt.description.each { |str| msg.write str + "\r\n" } } rescue Exception => err p err.class.to_s + " " + err.to_s return false end return true end end
So there we have it: a very simple use of Ruby to send emails. Obviously, the application itself is far from complete; all we have so far is three domain classes and their associated unit tests. We still need to design a user interface for this application to work with these domain classes and also design the data storage, either using files or a database.
Hopefully, by this stage you're getting a sense for the complexity that you'll have to deal with when creating software. Gracefully handling all of the weird and wonderful things that can go wrong is the key to successful software development.