在编写单元测试时,我们会遇到不同的外部依赖项,大体上可以分为两类:
我们将使用 Microsoft Fakes 分别对两种条件下的依赖项进行隔离。
依赖于接口或抽象类
首先,我们来定义被测试代码。
1 public interface IEmailSender
2 {
3 bool SendEmail(string content);
4 }
5
6 public class Customer
7 {
8 public string Name { get; set; }
9 public override string ToString()
10 {
11 return Name;
12 }
13 }
14
15 public interface ICustomerRepository
16 {
17 Customer Add(Customer customer);
18 }
19
20 public class CustomerRepository : ICustomerRepository
21 {
22 private IEmailSender _emailSender;
23
24 public CustomerRepository(IEmailSender emailSender)
25 {
26 _emailSender = emailSender;
27 }
28
29 public Customer Add(Customer customer)
30 {
31 _emailSender.SendEmail(customer.ToString());
32 return customer;
33 }
34 }
在上面的代码中,CustomerRepostory 依赖于 IEmailSender 接口。
当在 CustomerRepostory 中调用 Add 方法添加 Customer 时,将调用 IEmailSender 的 SendEmail 方法来发送一个邮件。
我们将如何为 Add 方法添加单元测试呢?
1 [TestMethod]
2 public void TestCustomerRepositoryWhenAddCustomerThenShouldSendEmail()
3 {
4 // Arrange
5 IEmailSender stubEmailSender = new EmailSender();
6
7 // Act
8 CustomerRepository repository = new CustomerRepository(emailSender);
9 Customer customer = new Customer() { Name = "Dennis Gao" };
10 repository.Add(customer);
11
12 // Assert
13 Assert.IsTrue(isEmailSent);
14 }
在这里,我们肯定不会使用这种直接实例化 EmailSender 的方法,因为这样就依赖了具体的类了。
1 IEmailSender stubEmailSender = new EmailSender();
现在,我们使用 Microsoft Fakes 中的 Stub 功能来帮助测试。
在测试工程的引用列表中,在被测试程序集上点击右键,选择 "Add Fakes Assembly"。

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

下一步,在测试类中添加 {被测试工程名称}.Fakes 名空间。
1 using ConsoleApplication17_TestFakes;
2 using ConsoleApplication17_TestFakes.Fakes;
当在代码中输入 Stub 时,智能提示会显示出已经自动生成的 Stub 类了。

现在,我们就可以使用 Stub 功能来模拟 IEmailSender 接口了。
1 [TestMethod]
2 public void TestCustomerRepositoryWhenAddCustomerThenShouldSendEmail()
3 {
4 // Arrange
5 bool isEmailSent = false;
6 IEmailSender stubEmailSender = new StubIEmailSender()
7 {
8 SendEmailString = (content) =>
9 {
10 isEmailSent = true;
11 return true;
12 },
13 };
14
15 // Act
16 CustomerRepository repository = new CustomerRepository(stubEmailSender);
17 Customer customer = new Customer() { Name = "Dennis Gao" };
18 repository.Add(customer);
19
20 // Assert
21 Assert.IsTrue(isEmailSent);
22 }
依赖于具体类
生活不总是那么美好,当然不是所有代码都会遵循控制反转的原则。很多时候,我们仍然需要使用具体类。
比如,在如下的代码中,OrderRepository 中的 Add 方法直接构建一个 EmailSender ,然后调用其 SendEmail 方法来发送邮件。
1 public class Order
2 {
3 public long Id { get; set; }
4 public override string ToString()
5 {
6 return Id.ToString();
7 }
8 }
9
10 public interface IOrderRepository
11 {
12 Order Add(Order order);
13 }
14
15 public class EmailSender : IEmailSender
16 {
17 public bool SendEmail(string content)
18 {
19 return true;
20 }
21 }
22
23 public class OrderRepository : IOrderRepository
24 {
25 public OrderRepository()
26 {
27 }
28
29 public Order Add(Order order)
30 {
31 IEmailSender emailSender = new EmailSender();
32 emailSender.SendEmail(order.ToString());
33 return order;
34 }
35 }
现在,我们已经没有接口或者抽象类可用于模拟了,所以 Stub 在此种条件下也失去了作用。此时,Shim 上场了。Shim 是运行时方法拦截器,功能更加强大。通过 Shim 我们可以为任意类的方法或属性提供我们自己的实现。
1 [TestMethod]
2 public void TestOrderRepositoryWhenAddOrderThenShouldSendEmail()
3 {
4 // Arrange
5 bool isEmailSent = false;
6
7 using (ShimsContext.Create())
8 {
9 ShimEmailSender.AllInstances.SendEmailString = (@this, content) =>
10 {
11 isEmailSent = true;
12 return true;
13 };
14
15 // Act
16 OrderRepository repository = new OrderRepository();
17 Order order = new Order() { Id = 123 };
18 repository.Add(order);
19 }
20
21 // Assert
22 Assert.IsTrue(isEmailSent);
23 }
使用 Shim 时,需要先为其指定上下文范围,通过 ShimsContext.Create() 来创建。

通常,如果遇到使用 Shim 的情况,则说明代码或许写的有些问题,没有遵循控制反转原则等。
使用 Shim 来控制系统类
假设我们需要一个判断当天是否是全年最后一天的方法,我们把它定义在 DateTimeHelper 静态类中。
1 public static class DateTimeHelper
2 {
3 public static bool IsTodayLastDateOfYear()
4 {
5 DateTime today = DateTime.Now;
6 if (today.Month == 12 && today.Day == 31)
7 return true;
8 else
9 return false;
10 }
11 }
我们来为这个方法编写测试,显然需要两种条件。
1 [TestMethod]
2 public void TestTodayIsLastDateOfYear()
3 {
4 // Arrange
5
6 // Act
7 bool result = DateTimeHelper.IsTodayLastDateOfYear();
8
9 // Assert
10 Assert.IsTrue(result);
11 }
12
13 [TestMethod]
14 public void TestTodayIsNotLastDateOfYear()
15 {
16 // Arrange
17
18 // Act
19 bool result = DateTimeHelper.IsTodayLastDateOfYear();
20
21 // Assert
22 Assert.IsFalse(result);
23 }
这么看来,在运行这两条单元测试时,肯定是一个是通过,一个是不通过。

为了解决这个问题,我们需要为系统类 System.DateTime 添加 Shim 类。
同样在程序集的引用列表中,在 System 上点击右键 "Add Fakes Assembly"。
然后会生成 System.Fakes 文件。

在测试代码中添加名空间 System.Fakes。
现在,我们来修改代码,使用 Shim 来完成测试。
1 [TestMethod]
2 public void TestTodayIsLastDateOfYear()
3 {
4 // Arrange
5
6 // Act
7 bool result = false;
8 using (ShimsContext.Create())
9 {
10 ShimDateTime.NowGet = () => new DateTime(2013, 12, 31);
11 result = DateTimeHelper.IsTodayLastDateOfYear();
12 }
13
14 // Assert
15 Assert.IsTrue(result);
16 }
17
18 [TestMethod]
19 public void TestTodayIsNotLastDateOfYear()
20 {
21 // Arrange
22
23 // Act
24 bool result = false;
25 using (ShimsContext.Create())
26 {
27 ShimDateTime.NowGet = () => new DateTime(2013, 12, 9);
28 result = DateTimeHelper.IsTodayLastDateOfYear();
29 }
30
31 // Assert
32 Assert.IsFalse(result);
33 }
直接为 ShimDateTime 的 Now 属性 Get 来指定 Lambda 表达式函数。
1 ShimDateTime.NowGet = () => new DateTime(2013, 12, 31);
通过 Debug 我们可以看到,DateTime.Now 已经被成功的替换为指定的时间。

参考资料
本文转自匠心十年博客园博客,原文链接:http://www.cnblogs.com/gaochundong/p/unit_test_with_microsoft_fakes.html,如需转载请自行联系原作者