使用 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 原創技術文章,發表於博客園博客,未經作者本人允許禁止任何形式的轉載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM