本文為 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 已經被成功的替換為指定的時間。
參考資料
- 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
本文為 Dennis Gao 原創技術文章,發表於博客園博客,未經作者本人允許禁止任何形式的轉載。