在 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 容器的合理使用将极大的发挥依赖注入的能力。