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






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

背景
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 客户端后,调用 ISMTPClient
的 Send()
方法。但是,由于以下原因,此 void Send()
方法使单元测试的工作变得更加困难:
- 依赖于 Internet,因此,不仅会花费时间,而且当 Internet 通信出现问题时也会失败。
- 此方法发送带有附件的电子邮件作为输出。但是,检查电子邮件收件箱对于单元测试来说是不可行的,因为它可能需要一段时间才能实际到达收件箱,甚至更糟糕的是,可能不知道电子邮件收件人的登录凭据。
但是,单元测试的理念是仅测试一小段代码,并假设世界的其他部分已经过测试。因此,我们宁愿测试 Run()
方法的实际职责。特别是,我们可以通过检查以下两件事得出结论,我们的 Run()
方法正在执行:
MailMessage
按照期望的方式组成。- 传递给
ISMTPConfig
的SMTPConfig
包含期望的配置值。
因此,我们以以下方式编写 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' 来实现依赖注入。如果您还没有看过依赖注入并且想知道如何编写单元测试,我必须说,请先看看这个很棒的技术。
作为免责声明,我知道对于类似的问题可能还有其他好的解决方案,在这种情况下,我期待收到读者的来信。