fakemail is a fake mail server that captures emails as files for acceptance testing. This avoids the excessive configuration of setting up a real mail server and trying to extract mail queue content.
There are two versions of Fakemail, the first written in Perl and the second written in Python. Their features are identical. We maintain two versions simply because the Perl version will be more suitable for some users, whilst the Python version will be more suitable for others.
If you have had to test applications that send e-mails, for example as part of a web sign up process, you will know what an involved and tricky exercise that can be. Usually you have to sign up with a special e-mail address, have the mail go to the mail server and then read it back into the test with a POP/IMAP client. There are several downsides to this approach; you need to install extra software to interact with the POP server, you suffer from spurious failures due to reliance on external infrastructure, and it is very very slow.
Fakemail works by intercepting the mail before it leaves the machine by replacing your Mail Transfer Agent (MTA). It's a simple script run from the command line that you can launch from within your test framework.
fakemail --host=localhost --port=10025 --path=temp --backgroundMails are simply dumped to a directory of your choosing, here the a temporary directory, complete with all of the mail headers. Briefly, the port parameter is the port to listen on initially, the host parameter is not currently checked and the background flag tells fakemail to run as a daemon and to return the process ID of the detached process.
If you already have an SMTP server on your computer, then you will either need to disable it to free up port 25 or use another port. The second option means that your mail sending library must have the capability to select a port other than 25 by some sort of configuration.
There is no installation of the fakemail program itself, but it does have some prerequisites. The first is Perl itself and beyond the basic distribution you will also need the CPAN module Net::Server::Mail::SMTP. Once Perl is installed you can install this module with...
perl -MCPAN -e 'install Net::Server::Mail::SMTP'For fakemail itself all you have to do is unpack the SourceForge tarball and it's ready to run.
There are 3 packages to choose from, depending on your operating system. Only the use of the source package will be discussed here (the use of the Windows package should be self explanatory). Untar the package, then run the following command:
# python setup.py install
That will install the fakemail.py script into the standard location, probably /usr/bin. You can install it somewhere else by specifying the --prefix option as follows (recommended):
# python setup.py install --prefix=/usr/local
To confirm everything is working, we should send it an e-mail. Note that the Perl and Python versions have slightly different names to ensure that you can safely install them both alongside each other.
To start the Perl version open a terminal and type:
fakemail --host=localhost --port=10025 --path=.
To start the Python version type:
fakemail.py --host=localhost --port=10025 --path=.All of these arguments are required.
This is what they mean:
- The host parameter names the server as localhost. Although required, this is not yet acted upon.
- The port argument sets the port that fakemail will listen on for connections. It will listen on this port until it's process is killed. fakemail is a full INET server and can handle multiple simultaneous requests. Note you can only connect to ports below 1024 if you are the root user.
- The path is the directory where the captured mails will be saved. The name of the file is the "to" address of the incoming mail with the index appended. For example the second mail sent to "me@here" will be saved in the file "me@here.2"
telnet localhost 10025If everything is working we should see the response...
Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. 220 uno.home SMTP Net::Server::Mail (Perl) Service readyWe can manually enter SMTP commands to simulate a mail dispatch, although we have to send them in order. The commands are "HELO" to start the session, "MAIL From:" to set the sender, "RCPT To:" to set the target and "DATA" for the mail content itself. The "QUIT" command ends the session. Here is the full conversation with our commands in bold, demonstrated using the Perl version (the output from the Python version is slightly different, but the essence is the same).
220 uno.home SMTP Net::Server::Mail (Perl) Service ready HELO mailer 250 Requested mail action okay, completed MAIL From: me@here 250 sender me@here OK RCPT To: you@there 250 recipient you@there OK DATA 354 Start mail input; end with <CRLF>.<CRLF> A-header: Sample header Hello . 250 message queued QUIT 221 uno.home Service closing transmission channelWe must enter both a carriage return and a line feed for each mail header line, although most telnet programs will do that automatically.
After this sequence we can stop the fakemail terminal with a Control-C to interrupt the process. Because we set the fakemail path to the local directory, we should see a file labelled "you@there.1". Here is the contents of that file...
A-header: Sample header HelloYou should now be ready to use fakemail to make your testing easier.
I am going to use a SimpleTest (PHP) example here, but translation to JWebUnit or HtmlUnit should be straight forward.
Firstly we will need a web page to test...
<html>
<?php
require_once('class.phpmailer.php');
require_once('class.smtp.php');
if ($_GET['email']) {
$mail = new PHPMailer();
$mail->addAddress(trim($_GET['email']));
$mail->From = 'test@lastcraft.com';
$mail->Body = 'Hi!';
$mail->Subject = 'Hello';
$mail->Mailer = 'smtp';
$mail->Host = 'localhost';
$mail->Port = isset($_GET['port']) ? $_GET['port'] : 25;
if ($mail->send()) {
print 'Mail sent to <em>' . $_GET['email'] . '</em><br />';
}
}
?>
<form>
Enter your mail address:<br />
<input type="text" name="email" /><br />
<input type="submit" value="Send" />
</form>
</html>
This is not strictly test driven develoment of course, but things
are easier to explain if I show the script first.
Because the inbuilt PHP mail() function does not let you override the port, I am using the PHPMailer library. This library has it's own SMTP client making the choice of port easy to configure. I am taking the port as an extra form parameter, because we don't always have the freedom to disable mail transfer agents whilst testing.
Now the test case...
<?php
require_once('simpletest/web_tester.php');
require_once('simpletest/reporter.php');
class MailGreetingTest extends WebTestCase {
function setUp() {
$command = './fakemail --path=. --host=localhost --port=10025 --background';
$this->pid = `$command`;
@unlink('marcus@localhost.1');
}
function tearDown() {
$command = 'kill ' . $this->pid;
`$command`;
@unlink('marcus@localhost.1');
}
function testGreetingMailIsSent() {
$this->get('http://localhost/fakemail/docs/example/mail.php');
$this->setField('email', 'marcus@localhost');
$this->clickSubmit('Send', array('port' => 10025));
$this->assertWantedText('Mail sent to marcus@localhost');
$sent = file_get_contents('marcus@localhost.1');
list($headers, $content) = split("\r\n\r\n", $sent);
$this->assertTrue(trim($content) == 'Hi!');
}
}
$test = new MailGreetingTest();
$test->run(new HtmlReporter());
?>
The main point of course is that fakemail is started
and stopped for each test using setUp() and
tearDown().
In addition to keeping track of the process ID so as to
kill fakemail after each test, we also need to delete
the captured mails.
Otherwise the next mail will be saved as marcus@lastcraft.com.2
making our tests a erratic.
The local directory needs to be wroteable by the web server for
this to work.
The only tricky bit in the test is the clickSubmit() line. That's where we send the extra port parameter override for testing so that the mail is sent to fakemail rather than the regular mail transfer agent.
The actual test itself could be improved by some obvious refactoring. Creating a generic MailTestCase would involve more useful assertions such as assertMailText() and clickMailLink() for testing double opt-ins. For ease of illustration, I am just picking the captured mail apart to confirm the not very exciting content. Hopefully your own tests will be much more interesting.
