Struggling for Competence

Unit Testing with Email

Lots of applications send email. You might send email to customers when they request a forgotten password, or you might send an email to support when an unhanded exception occurs. The problem in unit testing is you don't want to actually send an email every time the test are run, but you may well want to test the logic around the email send. How can you stub out the email for unit testing?

You might have some code which looks a bit like this:


public void SendSupportMail(string toAddress)
{
    if (Regex.IsMatch(toAddress, @"^([0-9a-zA-Z]([-\.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$"))
    {
        MailMessage message = new MailMessage("support@bigcorp.com", toAddress);
        message.IsBodyHtml = false;
        message.Subject = "BigCorp support wants to help";
        message.Body = string.Format("BigCorp is dealing with your problem./nYour reference for this issue is {0}.", new Random().Next(0, 10000));
        SmtpClient client = new SmtpClient();
        client.Send(message);
    }
}

On it's own, maybe this code isn't so bad. It's simple enough that a unit test isn't going to provide huge value. The problem happens later, when the SendSupportMail call get's embedded in some more complex business logic. When you try to unit test that, emails start flying out, and you look a bit stupid.

The problem unit testing this code is the line "SmtpClient client = new SmtpClient()". Newing the SmtpClient binds the code to the real Smtp client. It's going to send out a real email and you can't avoid it. To make this unit-testable you need to fake out the SmtpClient. With a fake Smtp client in place, the rest of the logic can run ok, but when client.Send(message) is called nothing will actually happen. How do you do this?

A unit tested version of the code looks like this:


using System;
using System.Net.Mail;
using System.Text.RegularExpressions;
using MbUnit.Framework;

namespace EmailDemo
{
    [TestFixture]
    public class EmailTest
    {
        public interface ISmtpClient
        {
            void Send(MailMessage message);
        }

        public class RealSmtpClient : ISmtpClient
        {
            public void Send(MailMessage message)
            {
                SmtpClient client = new SmtpClient();
                client.Send(message);
            }
        }

        public class FakeSmtpClient : ISmtpClient
        {
            public bool MailSent { get; set; }
            public FakeSmtpClient()
            {
                MailSent = false;
            }
            public void Send(MailMessage message)
            {
                MailSent = true;
            }
        }

        public class EmailHelper
        {
            private ISmtpClient smtpClient;

            public EmailHelper(ISmtpClient smtpClient)
            {
                this.smtpClient = smtpClient;
            }

            public void SendSupportMail(string toAddress)
            {
                if (IsValid(toAddress))
                {
                    MailMessage message = MakeMessage(toAddress);
                    smtpClient.Send(message);
                }
            }

            public MailMessage MakeMessage(string toAddress)
            {
                MailMessage message = new MailMessage("support@bigcorp.com", toAddress);
                message.IsBodyHtml = false;
                message.Subject = "BigCorp Support wants to help";
                message.Body = string.Format("BigCorp is dealing with your problem./nYour reference for this issue is {0}.", new Random().Next(0, 10000));
                return message;
            }

            public bool IsValid(string emailAddress)
            {
                return Regex.IsMatch(emailAddress, @"^([0-9a-zA-Z]([-\.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$");
            }
        }

        [Test]
        public void IfEmailAddressIsValid_AnEmailShouldBeSent()
        {
            FakeSmtpClient fakeClient = new FakeSmtpClient();
            EmailHelper helper = new EmailHelper(fakeClient);

            helper.SendSupportMail("mat@littlecorp.com");

            Assert.IsTrue(fakeClient.MailSent);
        }

        [Test]
        public void IfEmailAddressIsInvalid_AnEmailShouldNotBeSent()
        {
            FakeSmtpClient fakeClient = new FakeSmtpClient();
            EmailHelper helper = new EmailHelper(fakeClient);

            helper.SendSupportMail("matatlittlecorp.com");

            Assert.IsFalse(fakeClient.MailSent);
        }
    }
}

There are a few things to say about this code:

To be absolutely clear, actually sending an email has not been tested. This technique allows you to test the other logic without sending an email. You can also check that a mail would have been sent, if the real Smtp client were plugged in.

To really test the email send, you need an integration test. Actually sending an email is important. You need to check that the Smtp client configuration is correct, and you want to make sure the real email looks right. The simplest way to integration test the code is to just execute it, with your own email address, and check your inbox. If you only need to check the format of the email you can write it to disk, instead of going via smtp.

Failing to put the real Smtp client under test is somewhat unsatisfactory. What would be really nice is if you could automate that integration test, so your Smtp configuration was tested. Phil Haack describes doing this with an open source Smtp cliemt, and a testing wrapper he wrote for it. More power to the Haack. What I can't help thinking though, is that the .net framework needs to be more accessible for testing in this area.

In the code shown here I chose to fake out just the call to the Smtp client. There's another way you could go - put an interface on the whole email helper class. The interface would be something like void SendMail(string from, string to, string subject, string message). With this approach the whole email creation process is behind the interface. This could well work better. The problem I've had in the past though, is the types of email sent out can vary quite a lot: Plain-text or html, attachments or not, CC's etc. By the time I'd finished I'd wrapped the whole MailMessage class, only badly. Still a perfectly legitimate approach.

The main thing to remember is, if you want to unit test you need to make the external resources replaceable.