在 TDD 的實踐中,總是要考慮類的依賴項的透明性(Transparent)和不透明性(Opaque),進而采用合理的方式提高代碼的可測試性。
不透明依賴
我們先看段前置條件代碼,以供后文使用。
1 public interface IUserProvider 2 { 3 IList<User> GetUserCollection(); 4 } 5 6 public class UserProvider : IUserProvider 7 { 8 public IList<User> GetUserCollection() 9 { 10 return new List<User>() 11 { 12 new User() 13 { 14 Name = "hello", 15 LastActivity = DateTime.Now.AddDays(-1), 16 }, 17 }; 18 } 19 } 20 21 public class User 22 { 23 public string Name { get; set; } 24 public DateTime LastActivity { get; set; } 25 }
現在,我們需要一個負責管理 User 的類 UserManager,其實現了接口 IUserManager。
1 public interface IUserManager 2 { 3 int NumberOfUsersActiveInLast10Days(string userName); 4 } 5 6 public class UserManager : IUserManager 7 { 8 public int NumberOfUsersActiveInLast10Days(string userName) 9 { 10 IUserProvider userProvider = ServiceLocator.Current.GetInstance<IUserProvider>(); 11 IList<User> userCollection = userProvider.GetUserCollection(); 12 int result = 0; 13 foreach (User user in userCollection) 14 { 15 if (user.Name.StartsWith(userName) 16 && user.LastActivity > DateTime.Now.AddDays(-10)) 17 result++; 18 } 19 return result; 20 } 21 }
通過 UserManager 內定義的 函數 NumberOfUsersActiveInLast10Days 我們可以得到過去 10 天內活躍的用戶數量。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 IUnityContainer container = new UnityContainer(); 6 ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(container)); 7 8 container.RegisterType<IUserProvider, UserProvider>(new ContainerControlledLifetimeManager()); 9 container.RegisterType<IUserManager, UserManager>(new ContainerControlledLifetimeManager()); 10 11 UserManager userManager = new UserManager(); 12 int activeUserCount = userManager.NumberOfUsersActiveInLast10Days("hello"); 13 Console.WriteLine(activeUserCount); 14 15 Console.ReadKey(); 16 } 17 }
在函數 NumberOfUsersActiveInLast10Days 中,我們從 ServiceLocator 中獲取了一個 IUserProvider 的實現,然后通過其獲取所有 User。再根據給定條件過濾用戶,返回過去 10 天內的活躍用戶數量。
在 UserManager 的使用中,我們并不知道其依賴于 ServiceLocator 和 UserProvider 等類。
這種將 IoC 調用直接嵌入到代碼實現中的隱式使用方式稱之為不透明依賴注入。
測試不透明依賴
現在我們來為 NumberOfUsersActiveInLast10Days 編寫單元測試代碼。
第一個用例為驗證在數據庫中不存在用戶名以給定字符串開頭的用戶。
如果我不知道 NumberOfUsersActiveInLast10Days 的內部實現,采用黑盒測試的方式,我會寫出如下代碼。
1 [TestMethod] 2 public void GetActiveUsers_TestCaseOfZeroUsers_WouldReturnEmptyCollection() 3 { 4 // arrange 5 // no clear idea what to mock here 6 7 // act 8 var userManager = new UserManager(); 9 int numberOfUsers = userManager.NumberOfUsersActiveInLast10Days("x"); 10 11 // assert 12 Assert.IsTrue(numberOfUsers == 0); 13 }
則運行測試用例后,得到的結果是:
"未將對象引用設置到對象的實例。"
此時,我們知道了 NumberOfUsersActiveInLast10Days 函數還要依賴 ServiceLocator 和 UserProvider 類。
現在,我們來改進測試代碼。
1 [TestMethod] 2 public void GetActiveUsers_TestCaseOfZeroUsers_WouldReturnEmptyCollection() 3 { 4 // arrange 5 IUnityContainer container = new UnityContainer(); 6 ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(container)); 7 8 IUserProvider userProvider = Substitute.For<IUserProvider>(); 9 userProvider.GetUserCollection().Returns<IList<User>>(new List<User>()); 10 container.RegisterInstance<IUserProvider>(userProvider, new ContainerControlledLifetimeManager()); 11 12 // act 13 var userManager = new UserManager(); 14 int numberOfUsers = userManager.NumberOfUsersActiveInLast10Days("x"); 15 16 // assert 17 Assert.IsTrue(numberOfUsers == 0); 18 }
則現在我們可以通過此測試了。
透明依賴
可以看到,在代碼中使用不透明依賴將導致為代碼編寫單元測試變得困難和不可預測。
現在我來將依賴項重構為透明依賴,通過構造函數將依賴注入。
1 public class UserManager : IUserManager 2 { 3 private readonly IUserProvider _userProvider; 4 5 public UserManager(IUserProvider userProvider) 6 { 7 _userProvider = userProvider; 8 } 9 10 public int NumberOfUsersActiveInLast10Days(string userName) 11 { 12 IList<User> userCollection = _userProvider.GetUserCollection(); 13 int result = 0; 14 foreach (User user in userCollection) 15 { 16 if (user.Name.StartsWith(userName) 17 && user.LastActivity > DateTime.Now.AddDays(-10)) 18 result++; 19 } 20 return result; 21 } 22 }
代碼的使用也需稍作修改。
1 UserManager userManager = new UserManager(container.Resolve<IUserProvider>()); 2 int activeUserCount = userManager.NumberOfUsersActiveInLast10Days("hello"); 3 Console.WriteLine(activeUserCount);
這種可以明確的通過構造函數顯式的注入的依賴項稱之為透明依賴。
測試透明依賴
改進測試代碼,直接去掉了對 ServiceLocator 的依賴。
1 [TestMethod] 2 public void GetActiveUsers_TestCaseOfZeroUsers_WouldReturnEmptyCollection() 3 { 4 // arrange 5 IUserProvider userProvider = Substitute.For<IUserProvider>(); 6 userProvider.GetUserCollection().Returns<IList<User>>(new List<User>()); 7 8 // act 9 var userManager = new UserManager(userProvider); 10 int numberOfUsers = userManager.NumberOfUsersActiveInLast10Days("x"); 11 12 // assert 13 Assert.IsTrue(numberOfUsers == 0); 14 }
這一次運行順利的通過。
結論
通過使用透明依賴方式,可以極大的簡化測試編寫過程,并且可以引導更簡潔的設計。同時,配合 IoC 容器的合理使用將極大的發揮依賴注入的能力。
參考資料
文章列表