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?

  • Make an Interface for the Smtp client.
  • Make two implementations for the Smtp client:
    • Real – calls the real Smtp client, for the real code.
    • Fake – for unit testing.
  • Pass the interface into the constructor of the email helper.
  • Adjust the code to use the interface.
  • Add tests.

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:

  • I’ve separated the creation of the message, from the send of the message – doing this always seems to make the code clearer.
  • I’ve extracted the regex into an IsValid method, which better expresses the intention of the code. It also makes it easy to add tests for just the regex.
  • I’ve copied the regex from a reasonably reputable source, so I’m not really testing it properly. If I wrote it myself, it would have a bunch of dedicated tests – regex always goes wrong.
  • Validating email addresses with regex is quite a controversial area. Most regular expressions for validating email addresses are too strict. The problem is if you do it correctly you end up with something like this.
  • I’m ambivalent about testing the MakeMessage method. On the one hand it’s a method that you could write test for, and that would make me feel good. On the other I don’t think it would actually help – you really need to see a real email, to check it is formatted correctly.
  • This code doesn’t actually test sending an email.

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.

2 thoughts on “Unit Testing with Email

  1. Hi just wanted to give you a quick heads up and let you know a few of the
    images aren’t loading properly. I’m not sure why but I think its a linking issue.

    I’ve tried it in two different web browsers and both show the same outcome.

    My web page – domain naming

  2. Thanks for this article.

    I am new to unit testing and I have started using Mail4Net (www.mail4net.com), which provides a client for unit testing. It also has other functionality, which allows you to save emails to a folder or to a SQL database.

    This seems to be a good tool and I wondered if you knew about it and what you think?

    Sam

Leave a Reply

Your email address will not be published. Required fields are marked *

*


You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>