您的位置:首页 > 其它

使用 Microsoft Fakes 进行单元测试

2013-12-10 08:41 267 查看
[b]本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。[/b]

在编写单元测试时,我们会遇到不同的外部依赖项,大体上可以分为两类:

依赖于接口或抽象类

依赖于具体类

我们将使用 Microsoft Fakes 分别对两种条件下的依赖项进行隔离。

依赖于接口或抽象类

首先,我们来定义被测试代码。

public interface IEmailSender
{
bool SendEmail(string content);
}

public class Customer
{
public string Name { get; set; }
public override string ToString()
{
return Name;
}
}

public interface ICustomerRepository
{
Customer Add(Customer customer);
}

public class CustomerRepository : ICustomerRepository
{
private IEmailSender _emailSender;

public CustomerRepository(IEmailSender emailSender)
{
_emailSender = emailSender;
}

public Customer Add(Customer customer)
{
_emailSender.SendEmail(customer.ToString());
return customer;
}
}


在上面的代码中,CustomerRepostory 依赖于 IEmailSender 接口。

当在 CustomerRepostory 中调用 Add 方法添加 Customer 时,将调用 IEmailSender 的 SendEmail 方法来发送一个邮件。

我们将如何为 Add 方法添加单元测试呢?

[TestMethod]
public void TestCustomerRepositoryWhenAddCustomerThenShouldSendEmail()
{
// Arrange
IEmailSender stubEmailSender = new EmailSender();

// Act
CustomerRepository repository = new CustomerRepository(emailSender);
Customer customer = new Customer() { Name = "Dennis Gao" };
repository.Add(customer);

// Assert
Assert.IsTrue(isEmailSent);
}


在这里,我们肯定不会使用这种直接实例化 EmailSender 的方法,因为这样就依赖了具体的类了。

IEmailSender stubEmailSender = new EmailSender();


现在,我们使用 Microsoft Fakes 中的 Stub 功能来帮助测试。

在测试工程的引用列表中,在被测试程序集上点击右键,选择 "Add Fakes Assembly"。



然后会新增一个 Fakes 目录,并生成一个带 .Fakes 的文件。



下一步,在测试类中添加 {被测试工程名称}.Fakes 名空间。

using ConsoleApplication17_TestFakes;
using ConsoleApplication17_TestFakes.Fakes;


当在代码中输入 Stub 时,智能提示会显示出已经自动生成的 Stub 类了。



现在,我们就可以使用 Stub 功能来模拟 IEmailSender 接口了。

[TestMethod]
public void TestCustomerRepositoryWhenAddCustomerThenShouldSendEmail()
{
// Arrange
bool isEmailSent = false;
IEmailSender stubEmailSender = new StubIEmailSender()
{
SendEmailString = (content) =>
{
isEmailSent = true;
return true;
},
};

// Act
CustomerRepository repository = new CustomerRepository(stubEmailSender);
Customer customer = new Customer() { Name = "Dennis Gao" };
repository.Add(customer);

// Assert
Assert.IsTrue(isEmailSent);
}


依赖于具体类

生活不总是那么美好,当然不是所有代码都会遵循控制反转的原则。很多时候,我们仍然需要使用具体类。

比如,在如下的代码中,OrderRepository 中的 Add 方法直接构建一个 EmailSender ,然后调用其 SendEmail 方法来发送邮件。

public class Order
{
public long Id { get; set; }
public override string ToString()
{
return Id.ToString();
}
}

public interface IOrderRepository
{
Order Add(Order order);
}

public class EmailSender : IEmailSender
{
public bool SendEmail(string content)
{
return true;
}
}

public class OrderRepository : IOrderRepository
{
public OrderRepository()
{
}

public Order Add(Order order)
{
IEmailSender emailSender = new EmailSender();
emailSender.SendEmail(order.ToString());
return order;
}
}


现在,我们已经没有接口或者抽象类可用于模拟了,所以 Stub 在此种条件下也失去了作用。此时,Shim 上场了。Shim 是运行时方法拦截器,功能更加强大。通过 Shim 我们可以为任意类的方法或属性提供我们自己的实现。

[TestMethod]
public void TestOrderRepositoryWhenAddOrderThenShouldSendEmail()
{
// Arrange
bool isEmailSent = false;

using (ShimsContext.Create())
{
ShimEmailSender.AllInstances.SendEmailString = (@this, content) =>
{
isEmailSent = true;
return true;
};

// Act
OrderRepository repository = new OrderRepository();
Order order = new Order() { Id = 123 };
repository.Add(order);
}

// Assert
Assert.IsTrue(isEmailSent);
}


使用 Shim 时,需要先为其指定上下文范围,通过 ShimsContext.Create() 来创建。



通常,如果遇到使用 Shim 的情况,则说明代码或许写的有些问题,没有遵循控制反转原则等。

使用 Shim 来控制系统类

假设我们需要一个判断当天是否是全年最后一天的方法,我们把它定义在 DateTimeHelper 静态类中。

public static class DateTimeHelper
{
public static bool IsTodayLastDateOfYear()
{
DateTime today = DateTime.Now;
if (today.Month == 12 && today.Day == 31)
return true;
else
return false;
}
}


我们来为这个方法编写测试,显然需要两种条件。

[TestMethod]
public void TestTodayIsLastDateOfYear()
{
// Arrange

// Act
bool result = DateTimeHelper.IsTodayLastDateOfYear();

// Assert
Assert.IsTrue(result);
}

[TestMethod]
public void TestTodayIsNotLastDateOfYear()
{
// Arrange

// Act
bool result = DateTimeHelper.IsTodayLastDateOfYear();

// Assert
Assert.IsFalse(result);
}


这么看来,在运行这两条单元测试时,肯定是一个是通过,一个是不通过。



为了解决这个问题,我们需要为系统类 System.DateTime 添加 Shim 类。

同样在程序集的引用列表中,在 System 上点击右键 "Add Fakes Assembly"。

然后会生成 System.Fakes 文件。



在测试代码中添加名空间 System.Fakes。

using System.Fakes;


现在,我们来修改代码,使用 Shim 来完成测试。

[TestMethod]
public void TestTodayIsLastDateOfYear()
{
// Arrange

// Act
bool result = false;
using (ShimsContext.Create())
{
ShimDateTime.NowGet = () => new DateTime(2013, 12, 31);
result = DateTimeHelper.IsTodayLastDateOfYear();
}

// Assert
Assert.IsTrue(result);
}

[TestMethod]
public void TestTodayIsNotLastDateOfYear()
{
// Arrange

// Act
bool result = false;
using (ShimsContext.Create())
{
ShimDateTime.NowGet = () => new DateTime(2013, 12, 9);
result = DateTimeHelper.IsTodayLastDateOfYear();
}

// Assert
Assert.IsFalse(result);
}


直接为 ShimDateTime 的 Now 属性 Get 来指定 Lambda 表达式函数。

ShimDateTime.NowGet = () => new DateTime(2013, 12, 31);


通过 Debug 我们可以看到,DateTime.Now 已经被成功的替换为指定的时间。



参考资料

Isolating Code Under Test with Microsoft Fakes

Better Unit Testing with Microsoft Fakes

Visual studio 2012 Fakes

Using shims to isolate your application from other assemblies for unit testing

[b]本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。[/b]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: