• 使用 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 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。

  • 相关阅读:
    存储与服务器的连接方式对比(DAS,NAS,SAN)
    FreeNAS系统总结
    FreeNAS-9.10虚拟机测试安装
    rsync实时同步服务部署
    无限循环与嵌套循环
    几种循环语句
    选择结构if
    java引用数据类型
    java运算符的优先级
    java运算符-逻辑、三元运算符
  • 原文地址:https://www.cnblogs.com/gaochundong/p/unit_test_with_microsoft_fakes.html
Copyright © 2020-2023  润新知