使用 Microsoft Fakes 进行单元测试

本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。

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

  • 依赖于接口或抽象类
  • 依赖于具体类

我们将使用 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。

1 using 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 已经被成功的替换为指定的时间。

参考资料

本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。

posted @ 2013-12-10 08:41  sangmado  阅读(5746)  评论(10编辑  收藏  举报