Click here to Skip to main content
Click here to Skip to main content

Tagged as

UnitTestContext - Use of a Simple Yet Powerful Way to Unit Test Void Methods using Mock Objects

, 7 Jan 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
This article presents a simple yet very handy way to write unit tests for void methods.

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.

UnitTestContext_Seq_Diagram.jpg

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.

//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 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
{
    //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 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:

  1. Depends on Internet, so, it will not only take time but also fail when there is a problem on the Internet communication. 
  2. 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:

  1. The MailMessage is composed as desired.
  2. 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();
//Assign the mock implementation 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 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

S. M. SOHAN
Other ThoughtWorks
Canada Canada
Consultant
Read my blog at http://smsohan.blogspot.com
Follow on   Twitter

Comments and Discussions

 
GeneralMocking Framework PinmemberUrs Enzler7-Jan-08 2:38 
GeneralRe: Mocking Framework PinmemberS. M. SOHAN9-Jan-08 18:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141030.1 | Last Updated 8 Jan 2008
Article Copyright 2008 by S. M. SOHAN
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid