Introduction
I was very much impressed with Unit Tests at my first look for its easy constructs. I was fascinated by the way code is tested by means of only a few simple Assert methods. However, soon I started to feel the heat of this simplicity. Especially, writing unit tests for void
methods has never been easy and each time I had written one, I was never quite satisfied with the actual payout of the test. The following paragraphs will show you one way out of this discomfort through the use of UnitTestContext
and mock implementations.
Background
UnitTestContext
is a simple static
class that can be used as a shared memory among different classes. A void method of a mock implementation [of an interface] may utilize this shared memory to have some sort of return mechanism at the same time as complying to the interface. Then, we can write assertions on the UnitTestContext
to codify our assumptions though unit testing.
An Example Void Method
The following is a void
method that is intended to be a template method for email sender classes.
public virtual void Run()
{
try
{
System.Net.Mail.MailMessage message = createEmail(this.EmailAppType);
message = addAttachment(message);
ISMTPClient client = getSMTPClient(this.EmailAppType);
client.Send(message);
}
catch (Exception ex)
{
throw ex;
}
finally
{
OnRunCompleted();
}
}
The problem with this code is that this method is void
and it takes no arguments, making it difficult to write Assert calls. Let's take a deeper look into this and find out an easy and effective approach to unit test this method.
Dependency Injection and Use of Interfaces
The above code uses an interface named ISMTPClient
for injecting the dependency on System.Net.Mail.SMTPClient
. We will use this interface to create actual and mock implementations and inject the implementations using Dependency Injection techniques.
public interface ISMTPClient
{
SMTPConfig SmtpConfig
{
get;
set;
}
void Send(MailMessage message);
}
This interface uses the following SMTPConfig
class to hold SMTP configuration attributes.
public class SMTPConfig
{
public SMTPConfig()
{
}
public SMTPConfig(int id, string name, string host, int port,
string userName, string password, bool requiresSSL )
{
_smtpConfigID = id;
_smtpConfigName = name;
_host = host;
_port = port;
_userName = userName;
_password = password;
_requiresSSL = requiresSSL;
}
private int _smtpConfigID;
public int SMTPConfigID
{
get { return _smtpConfigID; }
set { _smtpConfigID = value; }
}
private string _smtpConfigName;
public string SMTPConfigName
{
get { return _smtpConfigName; }
set { _smtpConfigName = value; }
}
private string _host;
public string Host
{
get { return _host; }
set { _host = value; }
}
private int _port;
public int Port
{
get { return _port; }
set { _port = value; }
}
private string _userName;
public string UserName
{
get { return _userName; }
set { _userName = value; }
}
private string _password;
public string Password
{
get { return _password; }
set { _password = value; }
}
private bool _requiresSSL;
public bool RequiresSSL
{
get { return _requiresSSL; }
set { _requiresSSL = value; }
}
}
Now that we have defined the interface ISMTPClient
, we will create two implementations of this interface. One is SMTPEmailClient
that we will be using in our production code. And the other is MockSMTPClient
that we will be using for testing purposes only.
Let's take a look at the SMTPEmailClient
implementation first:
internal class SMTPEmailClient :ISMTPClient
{
private SMTPConfig _smtpConfig;
public SMTPConfig SmtpConfig
{
get { return _smtpConfig; }
set { _smtpConfig = value; }
}
private SmtpClient _smtpClient;
public SMTPEmailClient()
{
}
public SMTPEmailClient(SMTPConfig config)
{
_smtpConfig = config;
}
#region ISMTPClient Members
public void Send(System.Net.Mail.MailMessage message)
{
configureClient();
try
{
_smtpClient.Send(message);
}
catch (Exception ex)
{
throw ex;
}
}
#endregion
private void configureClient()
{
if (_smtpConfig == null)
{
throw new Exception("Failed to configure SMTPClient.
SmtpConfig is Null. Please provide appropriate value");
}
_smtpClient = new SmtpClient(_smtpConfig.Host, _smtpConfig.Port);
_smtpClient.EnableSsl = _smtpConfig.RequiresSSL;
NetworkCredential credential = new NetworkCredential
(_smtpConfig.UserName, _smtpConfig.Password);
_smtpClient.Credentials = credential;
}
}
The template method 'Run()
' hits the Send()
method of ISMTPClient
after it composes a mail and configures the SMTP client. But this void Send()
method makes the job of unit testing much more difficult because of the following reasons:
- Depends on Internet, so, it will not only take time but also fail when there is a problem on the Internet communication.
- This method sends an email with attachment as an output. But checking the e-mail inbox is not feasible for unit testing because it may take a while to actually reach the inbox and even worse, the login credentials may not be known for the email recipients.
However, the philosophy of unit test is to test only a unit bunch of code and assume the rest of the world is already tested. So, we would rather want to test the actual responsibility of the Run()
method. Particularly we can conclude that our Run()
method is performing by checking the following two things:
- The
MailMessage
is composed as desired. - The
SMTPConfig
that's passed to the ISMTPConfig
contains the desired configuration values.
So, we write the mock implementation in the following way to help us in testing these two assumptions:
class MockSMTPClient : ISMTPClient
{
#region ISMTPClient Members
public void Send(System.Net.Mail.MailMessage message)
{
UnitTestContext.Current["Message"] = message;
}
#endregion
#region ISMTPClient Members
public VFSmoothie.Data.SMTPConfig SmtpConfig
{
get
{
return UnitTestContext.Current["SmtpConfig"];
}
set
{
UnitTestContext.Current["SmtpConfig"] = value;
}
}
#endregion
}
Thus, instead of sending the e-mail, this mock implementation just saves the message and SmtpConfig
in the UnitTestContext.Current Dictionary
. This removes the dependency on Internet communication and also provides us with the opportunity to write assertions on the two assumptions about the Run()
method. This facilitates us to write effective unit test for the Run()
method.
The UnitTestContext Class
Before I show you the example test code, please take a look at the following code for UnitTestContext
implementation:
public static class UnitTestContext
{
public static Dictionary<string, object> Current;
static UnitTestContext()
{
Current = new Dictionary<string, object>();
}
public static void Reset()
{
Current = new Dictionary<string, object>();
}
}
The Unit Test Code
Now that we have all the pieces ready to write unit tests, the following example lists some assertions on the UnitTestContext
to test the void
method 'Run()
'.
private SomeEmailSender _sender = null;
[SetUp]
public void Init()
{
_sender = new SomeEmailSender();
_sender.EmailClient = new MockSMTPClient();
UnitTestContext.Reset();
}
[Test]
public void TestRun()
{
try
{
_sender.Run();
EmailApp app = new EmailAppDAL().GetEmailApp(EmailAppType.SomeType);
SMTPConfig config = new SMTPConfigDAL().GetSMTPConfig(EmailAppType.SomeType);
List<EmailRecipient> toList = new EmailRecipientDAL().GetEmailRecipients
(EmailAppType.SomeType, AddressType.To);
List<EmailRecipient> ccList = new EmailRecipientDAL().GetEmailRecipients
(EmailAppType.SomeType, AddressType.CC);
MailMessage message = UnitTestContext.Current["Message"] as MailMessage;
Assert.AreEqual(app.FromEmail, message.From.Address);
Assert.AreEqual(app.FromName, message.From.DisplayName);
Assert.AreEqual(app.Subject, message.Subject);
Assert.AreEqual(app.Body, message.Body);
SMTPConfig smtpConfig = UnitTestContext.Current["SMTPConfig"] as SMTPConfig;
Assert.AreEqual(config.Host, smtpConfig.Host);
Assert.AreEqual(config.Port, smtpConfig.Port);
Assert.AreEqual(config.UserName, smtpConfig.UserName);
Assert.AreEqual(config.Password, smtpConfig.Password);
Assert.AreEqual(config.RequiresSSL, smtpConfig.RequiresSSL);
for(int i = 0; i < message.To.Count; i++)
{
Assert.AreEqual(message.To[i].Address, toList[i].EmailAddress);
}
for (int i = 0; i < message.CC.Count; i++)
{
Assert.AreEqual(message.CC[i].Address, ccList[i].EmailAddress);
}
}
catch (Exception)
{
Assert.Fail();
}
}
This way we can write unit tests for methods that seem unfit for regular unit testing directions. I hope this will be helpful for someone with similar needs.
Points of Interest
Dependency Injection techniques must be used to actually write effective unit tests. And I have used 'Spring.Net' for implementing Dependency Injection. If you haven't yet taken a look at Dependency Injection and are wondering about writing unit tests, I must say, please take a look at this cool technique first.
As a disclaimer, I know there may be other good solutions to similar problems and in that case I will look forward to hear from my readers.