接口模拟工具_模拟作为设计工具

本文探讨了模拟工具在设计中的作用,特别是在复杂系统和业务流程中的应用。通过模拟,可以有效地设计模块边界,区分组合与聚合关系,以及作为外部设计的一种手段。文中以电子商务网站的支付流程为例,展示了如何利用模拟来设计和测试不同的业务模块,如支付、订单和配送,强调了模拟在系统设计和测试中的重要性。
摘要由CSDN通过智能技术生成

接口模拟工具

模拟仍然是TDD从业者之间争论的焦点。 最大的抱怨是,当我们使用模拟程序时,测试最终会对被测模块的内部了解太多,从而使将来难以重构。 因此,一个常见的建议是避免使用模拟,而仅在以下情况下将其用作隔离机制:

  • 域模型:模拟六角形体系结构中的端口,保护您的域不受基础结构和交付机制的影响。
  • 系统:在系统边界进行模拟(I / O)
  • 分层:在每一层的边界进行模拟。

如果我们将模拟作为测试进行考虑,那么这是一个非常合理的建议,大多数TDD从业人员都会同意,包括我自己。

但是,当构建具有复杂域的系统时,业务流不是线性的(涉及域模型的不同区域的流,每个区域可能触发其自己的子流),因此,将模拟的使用限制在上述范围内并不意味着足够。

关联vs组成vs聚合

让我们看一下模块A,B和C之间的关系。

嘲笑

协会

上图描述了A与B和C 关联 。根据关联的方向,我们可以说A使用B和C,但是B和C不知道A。

但A的B和C 部分 ,或者只是使用一个?

组成

嘲笑

上图描述了B和C是A的一部分 ,这意味着它们的生命周期直接由A的生命周期控制。 如果从内存中甚至从代码库中删除了A,则B和C也将被删除,因为如果没有A,它们将毫无意义。

聚合

嘲笑

上面的图描述了B和C由A 中使用 ,这意味着,它们的生命周期不被A的生命周期的控制。 如果删除了A,则B和C将继续存在。

组合和聚合都是关联的类型。 这种区别很重要,因为它可以帮助我们理解模块或组件的边界。

界线

在谈论嘲弄之前,我们应该谈论边界。 某些边界非常清晰,例如包,名称空间,类,方法或函数。 它们是由我们的编程语言和范例强加的。 其他边界更为主观,这意味着它们是由我们的宏观设计选择定义的,例如层,六边形,组件,交付机制,领域模型,基础结构和总体上的设计模式。 TDD的从业者,包括通常不喜欢模拟的人,都乐于在模拟驾驶的边界使用模拟。 唯一的问题是,在测试代码时,他们很少同意应在哪里绘制边界。

让我们看一个例子

假设我们正在构建一个电子商务Web应用程序,并与产品负责人讨论后,我们捕获了以下高级要求:

方案:付款

为了将我的物品送到我的家庭住址
作为买家
我想支付购物篮中的所有物品

验收标准

  • 用户填写付款明细后,将从结帐页面触发方案。
  • 付款应根据以下条件处理:
    • 选择付款方式(信用卡,借记卡,贝宝,Apple Pay)
  • 应该创建一个包含用户,购物篮中的所有物品,折扣,付款方式和收货地址的订单。
    • 创建时应将其状态设置为“未清”,成功付款后应设置为“已付款”,否则应设置为“付款失败”。
  • 所有已售出的物品都应发送至仓库系统的通知。 仓库系统将触发交货过程。
  • 付款发送后,应再次检查交货日期,并在确认页面中将其显示给用户。 不同的物品可能在不同的日期交付。
  • 发送给用户的电子邮件确认,其中包含付款确认和交货日期。
  • 用户将看到带有订单号的确认页面,该订单号链接到订单详细信息,每个项目的交货日期,使用的付款方式和付款金额。

我们可以在接受标准中添加更多的行为,但是我想这足以作为一个功能(付款)的示例,在该功能中,来自浏览器的单个触发可以触发很多行为。

识别行为和模块

为了使本文的重点和简单性保持不变,我将仅关注上述业务逻辑,而不涉及实现细节。

在阅读以上要求时,我相信很明显,我们不会尝试将所有内容放入单个模块中。 因此,在我看来,以古典主义者的方式测试所有逻辑似乎是浪费时间。 上面的要求为我提供了足够的信息,可以做出一些宏设计决策,并在测试代码时更有效。

查看需求,我们可以确定两种不同类型的流:

  • 主流程:总体付款流程需要编排不同的子流程。
  • 子流程:主要流程的步骤。 这些步骤中的每一个都有自己的流程,并代表域的不同区域。 这些不同的领域可以称为模块。

让我们看一下潜在的模块和职责:

  • 付款:付款方式,付款网关,特定国家/地区的逻辑,欺诈检测。
  • 订单:创建订单,更新订单。
  • 交付:复杂的逻辑,其中购物篮中的不同项目可能会根据仓库位置,交付方式等在不同的日期进行分组和交付。
  • 通知:与付款,交付等有关的用户通知。

还需要将总体逻辑添加到模块中,并且应该将此逻辑提供给交付机制。

模拟作为设计工具

当构建具有许多可移动部件的复杂流程时,尝试一次性构建所有内容可能会非常复杂,因为我们需要在不同抽象级别上保持逻辑之间的交替。 例如,在测试驱动主要业务流程时,我们不应专注于欺诈检测的细节。 在这种情况下,模拟作为设计工具可能会非常有用。

当使用Outside-In TDD(伦敦学校)时,将模拟作为设计工具更为有意义。 这种TDD样式专注于将大问题分解为小问题。 它从宏行为(主流)开始,将其分解为较小的行为,直到分解的行为足够小以至于没有凝聚力或只有一个改变的理由。 在上面的示例中,我们将首先开始对主流进行测试驱动,然后才考虑模块内部的子流。

通过模拟进行外部设计

假设我们将有一个名为PaymentsAPI的终结点,它将解析通过HTTP请求接收的JSON,将调用主流程 ,并将HTTP代码和确认JSON返回到前端应用程序。

嘲笑

现在,我们需要定义将由PaymentsAPI调用的域模块。 目前,我们可以使用模拟来设计该模块的接口,并完成PaymentsAPI的实现。 我们可以安全地执行此操作的原因是,我们已经确定了什么行为将保留在PaymentsAPI以及将要委派什么。

如果您遵循“干净架构”,“领域驱动设计”或“交互驱动设计”,我们将需要一个可协调业务流程的模块。 该模块扮演用例(干净架构),应用程序服务(DDD)或操作(IDD)的角色。 我将使用IDD命名并将其命名为CheckoutAction

嘲笑

使用模拟设计CheckoutAction公共接口有助于我们确定PaymentsAPI应该发送什么以及需要接收什么。 这样,我们就可以完成对PaymentsAPI测试,而不必担心业务流程的细节。 这也使我们能够对某些值进行硬编码,并已经测试了整个用户界面过程。

设计领域模型

现在我们知道CheckoutAction是域模型的入口点,我们需要实现它。 根据我们之前的分析,我们至少有四类需要由CheckoutAction协调的行为:付款,订单,交付,通知。

组成还是聚集?

在查看与付款,订单,交付或通知有关的逻辑时,它们与CheckoutAction有何关系?

嘲笑

它们是CheckoutAction 一部分还是 CheckoutAction 使用 ? 这是一个重要的区别,因为对这个问题的答案直接影响我们围绕业务组件划分的边界,而边界又会影响测试策略。 如果边界定义得当,并且略微提前考虑可以使TDD更加有效,那么测试驱动代码就容易得多。

那么,回到问题所在:我们如何知道关联是组合还是聚集?

一种简单的方法是询问生命周期,这意味着,如果我们删除CheckoutAction ,是否还应该删除与付款,订单,交付和通知有关的逻辑? 我们需要非常小心这个问题,因为从理论上讲,如果CheckoutAction是唯一调用其他逻辑的代码,则它们将是孤立的,应将其删除。 但这还不足以确定一个关联是聚合还是合成。 更好的方法是考虑可变性。 是否出于相同原因更改了模块? 是否有其他演员对此感兴趣? 他们拥有不同的数据吗? 它们的变化是否会影响其他领域?

分析可变性

首先,让我们讨论CheckoutAction的职责。 如果我们尝试将与付款,订单,交付和通知相关的所有逻辑以及这些领域中的协调放在一起,则CheckoutAction将违反单一责任原则,可能还会违反其他十几个设计原则。 为了避免这种情况,我们应该将CheckoutAction控件保留在主流程中,但将每个步骤的行为委托给自己的模块。

现在,让我们看一下付款。 我们可以添加或删除付款方式,付款网关,特定国家/地区的逻辑以及欺诈检测。 在与企业交流时,我们发现在向平台添加新的付款方式或网关之前,将有一群人参与其中,与供应商签订合同,而这些人与涉及其他领域的人并不相同。系统。 这会影响结帐流程吗? 我宁愿Payments的更改不会影响其他领域。 这将使Payments模块与Open / Closed Principle兼容,这意味着我们可以添加或删除付款方式,网关等,而不会影响系统的其余部分。 考虑到这一点,我非常有信心使付款模块完全独立于CheckoutAction ,这意味着聚合。

嘲笑

那么, Orders呢? 在与业务部门交谈时,我们发现Orders具有自己的生命周期(新Orders ,等待确认Orders已付款Orders已拒绝Orders已履行Orders等),系统的不同部分以及内部用户需要订单信息,主要是在后台。 这是足够的信息,可用于决定将Orders逻辑与结帐流程分开。

嘲笑

交付是域的复杂部分,因为它涉及世界不同地区的不同服务提供商。 他们还收取不同的费率,具有不同的合同模型等。根据他们的送货地址来确定全世界的用户可以使用哪些送货选项并非易事。 此信息由其他后台团队维护。 假设应该也将Delivery与其他结帐流程隔离开来,这看起来很安全。

嘲笑

最后,我们讨论Notifications 。 我们从业务中获得的唯一一件事是,我们需要将结帐流程的结果发送给用户通知-要么已经收到付款,就记录了订单,要么拒绝了付款。 尽管我有一种直觉,认为这种逻辑可以增长并可以重用,但是我没有足够的证据,因此在这种情况下,我会将其保留为结帐流程的子模块。

嘲笑

为了收集上面的信息并决定是什么构成或聚合,我们只需要与产品所有者进行快速对话,就产品流程的每个部分向我们询问一些业务问题。

使用模拟来设计协作

决定什么行为组合在一起(组成)以及什么行为将在单独的模块中(聚合)的决定,我们只需要弄清楚这些模块之间如何进行对话即可。

嘲笑

在这一点上,我使用外部定义的TDD(伦敦学校)来测试CheckoutAction的完整实现,并使用模拟定义了PaymentsOrdersDelivery的公共接口。 它们的界面应限于CheckoutAction所需的信息,以减少耦合并增加模块之间的内聚性。

嘲笑

一旦CheckoutAction完全实现,我们还将定义其所有协作者的公共接口。 下一步将是考虑每个协作者的内部,只要他们的公共界面保持不变,其他任何因素都不会受到影响。

设计协作者(由内而外)

定义了三个模块后,我们现在可以选择第一个模块,例如Payments.makePayment(...)并进行探索。 我们可以提出类似的问题:是否可以将信用卡,Paypal和Direct Debit的逻辑相互隔离? 我们是否应将欺诈检测逻辑与付款方式隔离开? 如果我们要存储信用卡数据,他是否应该提供单独的服务并减少需要符合PCI要求的系统范围? 我们是否应该在系统区域中根据用户的位置来决定要使用哪个支付网关? 回答这些问题使我们决定Payments将具有多少个子模块以及它们之间的关联类型–组合或汇总。 如前所述,这将帮助我们确定模拟的内容,模拟的时间和模拟的位置。 有了宏设计的粗略概念,我们就可以开始测试“ Payments模块的驱动程序,以确保我们有足够的灵活性来调整设计思想,因为我们可以通过代码获得更多的见解。

最后的话

对于习惯于外部使用TDD和设计的人们, 模拟是一种设计工具,而不是测试工具。 一旦我们了解了这一点,模拟就成为驱动应用程序宏设计的绝佳工具。

当我们能够划定模块的边界时,测试驱动开发将变得更加容易。 不断发展复杂的系统,一次进行一项测试并不容易。 在实施功能之前快速分析业务并进行一些高级设计可以为我们节省大量时间。 一旦有了一个粗略的高层设计计划,我们的TDD工作就会变得更加集中和高效,几乎是机械化的-这是一件好事。

但是,如果边界不明确,我们该怎么办? 有时,我们根本无法看到解决方案的外观,因此需要进行探索。 在这种情况下,请不要创建高级设计,而直接转到Classicist TDD ,一步一步地工作,直到找到解决方案为止。 如果确实需要,请完全忘记TDD,创建一个新分支,然后开始输入并查看会发生什么。

对我而言,这完全取决于信心。 如果我可以清楚地看到模块的设计和关联的类型,则将对该模型进行试驾,并将使用模拟来设计不同模块之间的交互。 如果我看不到解决方案,或者对自己脑海中的解决方案不太确定,我将切换到探索模式,以小幅工作,创建一个小混乱,然后使用重构来决定如何组织我的代码更好。 尽管始终以小增量工作和重构听起来不错,但我发现它极其缓慢且效率低下,因此我混用了不同的TDD样式。

资源资源

有关“外在设计”的更多信息,请参阅我的交互驱动设计(IDD)演讲

要完整地演示“由内而外”的TDD,请看一下我的三部分屏幕广播: 第1 部分第2 部分第3部分

有关Outside-In TDD和Classicist之间差异的摘要,请参阅此博客文章

翻译自: https://www.javacodegeeks.com/2018/10/mocking-design-tool.html

接口模拟工具

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值