类依赖项的不透明性和透明性

简介:

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

参考资料








本文转自匠心十年博客园博客,原文链接:http://www.cnblogs.com/gaochundong/p/design_for_testability_transparent_and_opaque_dependencies.html,如需转载请自行联系原作者

目录
相关文章
|
Java
Java中级开发工程师 面试题?
Java中级开发工程师 面试题? 注意事项 本线上笔试题主要是考察应聘人员的编码规范、代码设计能力,非单纯考察程序结果是否符合要求; 请务必不要在网上抄摘相关代码;
523 0
Java中级开发工程师 面试题?
|
算法 C语言
【数据结构】赫夫曼树及其应用
【数据结构】赫夫曼树及其应用
【数据结构】赫夫曼树及其应用
|
7天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
6天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
321 130
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
|
18天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1331 8
|
5天前
|
监控 JavaScript Java
基于大模型技术的反欺诈知识问答系统
随着互联网与金融科技发展,网络欺诈频发,构建高效反欺诈平台成为迫切需求。本文基于Java、Vue.js、Spring Boot与MySQL技术,设计实现集欺诈识别、宣传教育、用户互动于一体的反欺诈系统,提升公众防范意识,助力企业合规与用户权益保护。
|
17天前
|
机器学习/深度学习 人工智能 前端开发
通义DeepResearch全面开源!同步分享可落地的高阶Agent构建方法论
通义研究团队开源发布通义 DeepResearch —— 首个在性能上可与 OpenAI DeepResearch 相媲美、并在多项权威基准测试中取得领先表现的全开源 Web Agent。
1412 87