Cor*_*lou 28 php oop dependencies unit-testing design-patterns
我承认,我没有经过单元测试......但我想.话虽如此,我有一个非常复杂的注册过程,我想优化,以便更容易进行单元测试.我正在寻找一种方法来构建我的类,以便将来可以更轻松地测试它们.所有这些逻辑都包含在MVC框架中,因此您可以假设控制器是从中实例化所有内容的根.
为了简化,我实际上要问的是如何设置一个系统,您可以使用CRUD更新管理任意数量的第三方模块.这些第三方模块都是RESTful API驱动的,响应数据存储在本地副本中.删除用户帐户之类的东西需要触发删除所有相关模块(我称之为提供者).这些提供者可能依赖于另一个提供者,因此删除/创建的顺序很重要.我对我应该专门用来支持我的应用程序的设计模式感兴趣.
注册跨越多个类并将数据存储在多个db表中.这是不同提供者和方法的顺序(它们不是静态的,只是为了简洁起见):
Provider::create('external::create-user')在特定提供商的特定步骤启动注册.第一个参数中的双冒号语法表示该类应该触发创建providerClass::providerMethod.我提出了一个一般假定Provider将与方法的接口create(),update(),delete()所有其他供应商将实现它.如何实例化这可能是你需要帮助我的东西.$user = Provider_External::createUser() 在外部API上创建用户,返回成功,用户存储在我的数据库中.$customer = Provider_Gapps_Customer::create($user) 在第三方API上创建客户,返回成功并在本地存储.$subscription = Provider_Gapps_Subscription::create($customer) 在第三方API上创建与先前创建的客户关联的订阅,返回成功并在本地存储.Provider_Gapps_Verification::get($customer, $subscription)从外部API检索行.该信息存储在本地.另一个电话是我正在跳过以保持简洁.Provider_Gapps_Verification::verify($customer, $subscription)执行外部API验证过程.其结果存储在本地.这是一个非常愚蠢的样本,因为实际代码依赖于至少6个外部API调用和10个本地数据库行在注册期间创建.在构造函数级别使用依赖注入是没有意义的,因为我可能需要在控制器中实例化6个类,而不知道我是否甚至需要它们.我想要完成的事情就像Provider::create('external')我只是指定启动注册的起始步骤.
如您所见,这只是注册过程的一个示例.我正在构建一个系统,我可以拥有数百个服务提供商(外部API模块),我需要注册,更新,删除等.这些提供商中的每一个都与用户帐户相关联.
我想以一种方式构建这个系统,我可以在触发创建新提供程序时指定操作顺序(步骤).换句话说,允许我指定在事件链中接下来触发哪个提供者/方法组合,因为创建可以跨越这么多步骤.目前,我通过主题/观察者模式发生了这一系列事件.我希望可能将此代码移动到数据库表,provider_steps在此列出每个步骤以及它的跟随success_step和failure_step(对于回滚和删除).该表如下所示:
# the id of the parent provider row
provider_id int(11) unsigned primary key,
# the short, slug name of the step for using in codebase
step_name varchar(60),
# the name of the method correlating to the step
method_name varchar(120),
# the steps that get triggered on success of this step
# can be comma delimited; multiple steps could be triggered in parallel
triggers_success varchar(255),
# the steps that get triggered on failure of this step
# can be comma delimited; multiple steps could be triggered in parallel
triggers_failure varchar(255),
created_at datetime,
updated_at datetime,
index ('provider_id', 'step_name')
Run Code Online (Sandbox Code Playgroud)
这里有很多决定......我知道我应该优先考虑组合而不是继承并创建一些接口.我也知道我可能需要工厂.最后,我在这里有很多域模型,所以我可能需要业务域类.我只是不确定如何将它们全部融合在一起,而不会在追求圣杯时造成彻底的混乱.
此外,db查询的最佳位置在哪里?
我已经有了每个数据库表的模型,但我很想知道在何处以及如何实例化特定的模型方法.
您已经在使用pub/sub模式,这似乎是合适的.除了上面的评论之外什么都没有,我会考虑将有序列表作为优先机制.
但是,每个用户都关注其家属的操作顺序以触发成功/失败,这仍然无法闻到.依赖关系通常看起来像属于树,而不是列表.如果将它们存储在树中(使用复合模式),则内置递归将能够通过首先清理其依赖项来清除每个依赖项.这样你就不再担心按清除顺序排列优先级 - 树会自动处理.
您可以使用树来存储pub/sub订阅者,就像使用列表一样容易.
使用测试驱动的开发方法可以满足您的需求,并确保您的整个应用程序不仅可以完全测试,而且可以通过测试来完全覆盖,以证明它可以满足您的需求.我首先要准确描述您需要做什么才能满足一个要求.
您知道要做的一件事是添加提供程序,因此TestAddProvider()测试似乎是合适的.请注意,此时它应该非常简单,并且与复合模式无关.一旦有效,您就知道提供者有依赖者.创建一个TestAddProviderWithDependent()测试,看看它是怎么回事.同样,它应该不复杂.接下来,您可能想要TestAddProviderWithTwoDependents(),这就是列表实现的地方.一旦它工作,你知道你希望Provider也是一个Dependent,所以一个新的测试将证明继承模型是有效的.从那里,你会添加足够的测试来说服自己添加提供者和家属的各种组合,以及异常条件的测试等.仅从测试和要求,你' d快速达到满足您需求的复合图案.在这一点上,我实际上打开了我的GoF副本,以确保我理解选择复合模式的后果,并确保我没有添加不适当的疣.
另一个已知的要求是删除提供程序,因此创建TestDeleteProvider()测试,并实现DeleteProvider()方法.您也不会让提供程序删除其依赖项,因此下一步可能是创建TestDeleteProviderWithADependent()测试.复合模式的递归在这一点上应该是明显的,你应该只需要更多的测试来说服自己,深度嵌套的提供者,空叶子,宽节点等等都将正确地清理自己.
我认为您的提供商需要实际提供他们的服务.是时候测试调用提供者(使用模拟提供程序进行测试),并添加确保他们可以找到依赖关系的测试.同样,复合模式的递归应该有助于构建依赖关系列表或正确调用正确提供程序所需的任何内容.
您可能会发现必须按特定顺序调用提供程序.此时,您可能需要为组合树中每个节点的列表添加优先级.或者,您可能需要构建一个完全不同的结构(例如链接列表)以按正确的顺序调用它们.使用测试并慢慢接近它.您可能仍然有人担心您在特定的外部规定的订单中删除家属.此时,您可以使用您的测试向怀疑者证明您将始终安全地删除它们,即使它们不是按照他们想的顺序.
如果你做得对,你以前的所有测试都应该继续通过.
然后是棘手的问题.如果您有两个共享依赖的提供程序,该怎么办?如果删除一个提供程序,它是否应该删除所有依赖项,即使其他提供程序需要其中一个?添加测试,并实施您的规则.我想我会通过引用计数来处理它,但也许你想要第二个实例的提供者副本,所以你永远不必担心分享孩子,你就这样简单.或者它可能永远不会成为您域中的问题.另一个棘手的问题是,您的提供商是否可以拥有循环依赖关系.你如何确保自己最终没有自我参照循环?编写测试并弄清楚.
在您了解完整个结构之后,您才会开始考虑用于描述此层次结构的数据.
这是我考虑的方法.它可能不适合你,但这是你的决定.
单元测试 通过单元测试,我们只想测试构成源代码的各个单元的代码,通常是 PHP 中的类方法或函数(单元测试概述)。这表明我们不想在单元测试中实际测试外部API,我们只想测试我们在本地编写的代码。如果您确实想要测试整个工作流程,您可能想要执行集成测试(集成测试概述),这是一个不同的野兽。
当您特别询问有关单元测试的设计时,我们假设您实际上指的是单元测试而不是集成测试,并提出有两种合理的方法来设计您的提供程序类。
存根输出 用(可选)返回配置的返回值的测试替身替换对象的做法称为存根。您可以使用存根“替换 SUT 所依赖的实际组件,以便测试具有 SUT 间接输入的控制点。这允许测试强制 SUT 沿着它可能不会执行的路径运行”。参考与示例
模拟对象 用测试替身替换对象来验证期望(例如断言已调用方法)的做法称为模拟。
您可以使用模拟对象“作为观察点,用于验证 SUT 执行时的间接输出。通常,模拟对象还包括测试存根的功能,因为如果满足以下条件,它必须将值返回给 SUT:它尚未通过测试,但重点是验证间接输出。因此,模拟对象不仅仅是一个测试存根加上断言;它的使用方式完全不同”。 参考与示例
我们的建议 为所有 Stubbing 和 Mocking 设计您的类。PHP 单元手册有一个很好的存根和模拟 Web 服务示例。虽然这并不能帮助您开箱即用,但它演示了如何为您正在使用的 Restful API 实现相同的功能。
数据库查询发生的最佳位置在哪里? 我们建议您使用 ORM,而不是自己解决这个问题。您可以轻松地 Google PHP ORM,并根据自己的需求做出自己的决定;我们的建议是使用 Doctrine,因为我们使用 Doctrine,它非常适合我们的需求,在过去的几年里,我们逐渐意识到 Doctrine 开发人员对这个领域的了解有多深,简单地说,他们比我们自己做得更好所以我们很高兴让他们为我们做这件事。
如果您不太明白为什么应该使用 ORM,请参阅为什么应该使用 ORM?然后谷歌同样的问题。如果您仍然觉得自己可以推出自己的 ORM 或以其他方式自己比专门从事数据库访问的人更好地处理数据库访问,我们希望您已经知道问题的答案。如果您觉得自己迫切需要处理它,我们建议您查看许多 ORM 的源代码(请参阅 Github 上的 Doctrine),并找到最适合您的场景的解决方案。
感谢您提出一个有趣的问题,我很感激。