65.9K
CodeProject 正在变化。 阅读更多。
Home

UnitTestContext - 使用简单的强大方法使用 Mock 对象单元测试 Void 方法

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.57/5 (3投票s)

2008年1月7日

CPOL

4分钟阅读

viewsIcon

19670

本文介绍了一种简单而方便的方法来为 void 方法编写单元测试。

引言

第一次接触单元测试时,它简单的构造给我留下了深刻的印象。我被仅通过几个简单的 Assert 方法来测试代码的方式所吸引。然而,很快我就开始感受到这种简单性带来的挑战。特别是,为 void 方法编写单元测试从来都不是一件容易的事,而且每次我编写完一个测试后,我总是对测试的实际效果不太满意。以下段落将向您展示一种通过使用 UnitTestContext 和 mock 实现来摆脱这种不适的方法。

UnitTestContext_Seq_Diagram.jpg

背景

UnitTestContext 是一个简单的 static 类,可以用作不同类之间的共享内存。接口的 mock 实现中的 void 方法可以利用这个共享内存来实现某种返回机制,同时符合接口规范。然后,我们可以在 UnitTestContext 上编写断言,通过单元测试来明确我们的假设。

一个 Void 方法的例子

以下是一个 void 方法,旨在作为电子邮件发送者类的模板方法。

//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();
    }
}

这段代码的问题在于这个方法是 void 并且不带任何参数,这使得编写 Assert 调用变得困难。让我们更深入地了解一下,并找到一种简单有效的方法来单元测试这个方法。

依赖注入和接口的使用

上面的代码使用了一个名为 ISMTPClient 的接口来注入对 System.Net.Mail.SMTPClient 的依赖。我们将使用这个接口来创建实际的和 mock 实现,并使用依赖注入技术注入这些实现。

public interface ISMTPClient
{
    //the SMTPConfig Attribute
    SMTPConfig SmtpConfig
    {
        get;
        set;
    }
    //The Send method - yet another void method!
    void Send(MailMessage message);
}

这个接口使用以下 SMTPConfig 类来保存 SMTP 配置属性。

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; }
    }
}

现在我们已经定义了接口 ISMTPClient,我们将创建该接口的两个实现。一个是 SMTPEmailClient,我们将在生产代码中使用。另一个是 MockSMTPClient,我们仅用于测试目的。

让我们先看一下 SMTPEmailClient 实现

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;
    }
}

模板方法 'Run()' 在组成邮件并配置 SMTP 客户端后,调用 ISMTPClientSend() 方法。但是,由于以下原因,此 void Send() 方法使单元测试的工作变得更加困难:

  1. 依赖于 Internet,因此,不仅会花费时间,而且当 Internet 通信出现问题时也会失败。
  2. 此方法发送带有附件的电子邮件作为输出。但是,检查电子邮件收件箱对于单元测试来说是不可行的,因为它可能需要一段时间才能实际到达收件箱,甚至更糟糕的是,可能不知道电子邮件收件人的登录凭据。

但是,单元测试的理念是仅测试一小段代码,并假设世界的其他部分已经过测试。因此,我们宁愿测试 Run() 方法的实际职责。特别是,我们可以通过检查以下两件事得出结论,我们的 Run() 方法正在执行:

  1. MailMessage 按照期望的方式组成。
  2. 传递给 ISMTPConfigSMTPConfig 包含期望的配置值。

因此,我们以以下方式编写 mock 实现,以帮助我们测试这两个假设:

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
}

因此,这个 mock 实现没有发送电子邮件,而是简单地将消息和 SmtpConfig 保存在 UnitTestContext.Current Dictionary 中。这消除了对 Internet 通信的依赖,并且还为我们提供了机会对关于 Run() 方法的两个假设编写断言。这有助于我们为 Run() 方法编写有效的单元测试。

UnitTestContext 类

在我向您展示示例测试代码之前,请先查看以下 UnitTestContext 实现的代码

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>();
    }
}

单元测试代码

现在我们已经准备好所有编写单元测试的组件,以下示例列出了 UnitTestContext 上的一些断言,以测试 void 方法 '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();
    }
}

这样,我们可以为那些似乎不适合常规单元测试方向的方法编写单元测试。我希望这对有类似需求的人有所帮助。

关注点

必须使用依赖注入技术才能实际编写有效的单元测试。我使用 'Spring.Net' 来实现依赖注入。如果您还没有看过依赖注入并且想知道如何编写单元测试,我必须说,请先看看这个很棒的技术。

作为免责声明,我知道对于类似的问题可能还有其他好的解决方案,在这种情况下,我期待收到读者的来信。

© . All rights reserved.