|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionI was very much impressed about Unit Test at my first look for its easy constructs. I was fascinated by the way a 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.
BackgroundUnitTestContext 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 MethodThe following is a void method that is intended to be a template method for email sender classes. //A template method for all classes sending emails
///////////////////////////////////////////
//
//Template Method
//
///////////////////////////////////////////
// 1. Configure the Mail Message (to, cc, subject, body)
// 2. Create and add the attachment file
// 3. Configure the SmtpClient
// 4. Send the Message
// 5. Perform clean up
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, 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 InterfacesThe above code uses an interface named ISMTPClient to for injecting the dependency on System.Net.Mail.SMTPClient. We will use this interface to create actual and mock implementations and inject the implementions using Dependency Injection techniques. public interface ISMTPClient
{
//the SMTPConfig Attribute
SMTPConfig SmtpConfig
{
get;
set;
}
//The Send method - yet another void method!
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 purposed only. Lets 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 difficult because of the following reasons- I) Depends on Internet. So, it will not only take time but also fail when there is a problem on the internet communication. II) This method sends is 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. Particulary we can conclude that our Run() method is performing by checking the following two things - I) The MailMessage is composed as desired. II) The SMTPConfig thats passed to the ISMTPConfig contains the desired configuration values. So, we write the mock implementaion in the following way to help us 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 classBrefore 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 CodeNow 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();
//Assign the mock implementaion of the ISMTPClient
_sender.EmailClient = new MockSMTPClient();
//Start with an empty Unit Test Context
UnitTestContext.Reset();
}
[Test]
public void TestRun()
{
try
{
// This will hit MockSMTPClient's Send method
_sender.Run();
//Load the expected Test Data
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);
//Make Assertions
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 wondering about writing
unit tests, I must say, please take a look at this cool technique first.
|
|||||||||||||||||||||||||||||||||||||||||||||||