zoukankan      html  css  js  c++  java
  • 类依赖项的不透明性和透明性

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

    参考资料

  • 相关阅读:
    Vue条件判断
    揭秘webpack plugin
    vue实现网络图片瀑布流 + 下拉刷新 + 上拉加载更多
    npx 是什么?
    PAT 1100 Mars Numbers[难]
    PAT 1075 PAT Judge[比较]
    PAT 1083 List Grades[简单]
    PAT 1082 Read Number in Chinese[难]
    PAT 1135 Is It A Red-Black Tree[难]
    PAT 1127 ZigZagging on a Tree[难]
  • 原文地址:https://www.cnblogs.com/gaochundong/p/design_for_testability_transparent_and_opaque_dependencies.html
Copyright © 2011-2022 走看看