如何构建我的类以便更容易进行单元测试?

时间:2013-05-30 01:35:21

标签: php oop unit-testing design-patterns dependencies

我会承认,我没有进行过多次单元测试...但我想。话虽如此,我有一个非常复杂的注册过程,我想优化,以便更容易进行单元测试。 我正在寻找一种方法来构建我的类,以便将来可以更轻松地测试它们。所有这些逻辑都包含在MVC框架中,因此您可以假设控制器是根所有东西都从中实例化。

为了简化,我基本上要问的是如何设置一个系统,您可以使用CRUD更新管理任意数量的第三方模块。这些第三方模块都是RESTful API驱动的,响应数据存储在本地副本中。删除用户帐户之类的东西需要触发删除所有相关模块(我称之为提供者)。这些提供者可能依赖于另一个提供者,因此删除/创建的顺序很重要。 我对我应该专门用来支持我的应用程序的设计模式感兴趣

注册跨越多个类并将数据存储在多个db表中。以下是不同提供者和方法的顺序(它们不是静态的,只是为了简洁而写的)

  1. Provider::create('external::create-user')在特定提供商的特定步骤启动注册。第一个参数中的双冒号语法表示该类应该在providerClass::providerMethod上触发创建。我做了一个普遍的假设,Provider将成为所有其他提供商实现它的方法create()update()delete()的接口。 如何实例化这可能是您需要帮助我的。
  2. $user = Provider_External::createUser()在外部API上创建用户,返回成功,用户存储在我的数据库中。
  3. $customer = Provider_Gapps_Customer::create($user)在第三方API上创建客户,返回成功并在本地存储。
  4. $subscription = Provider_Gapps_Subscription::create($customer)在第三方API上创建与先前创建的客户相关联的订阅,返回成功并在本地存储。
  5. Provider_Gapps_Verification::get($customer, $subscription)从外部API检索一行。该信息存储在本地。另一个电话是我正在跳过以保持简洁。
  6. Provider_Gapps_Verification::verify($customer, $subscription)执行外部API验证过程。其结果存储在本地。
  7. 这是一个非常愚蠢的样本,因为实际代码依赖于至少6个外部API调用和10个本地数据库行在注册期间创建。在构造函数级别使用依赖注入是没有意义的,因为我可能需要在控制器中实例化6个类,而不知道我是否甚至需要它们。我想要实现的目标是Provider::create('external'),我只需指定启动注册的起始步骤。


    问题的症结

    正如您所看到的,这只是注册过程的一个示例。我正在建立一个系统,我可以拥有数百个服务提供商(外部API模块),我需要注册,更新,删除等。这些提供商中的每一个都与用户帐户相关联。

    我想在触发创建新提供程序时指定操作顺序(步骤)的方式构建此系统。换句话说,允许我指定在事件链中接下来触发哪个提供者/方法组合,因为创建可以跨越这么多步骤。目前,我通过主题/观察者模式发生了这一系列事件。我希望可能将此代码移动到数据库表provider_steps,其中我列出了每个步骤以及它跟随success_stepfailure_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')
    

    这里有很多决定......我知道我应该更喜欢组合而不是继承并创建一些接口。我也知道我可能需要工厂。最后,我在这里有很多域模型,所以我可能需要业务域类。我只是不确定如何将它们全部融合在一起,而不会在追求圣杯时造成彻底的混乱。

    此外,数据库查询最佳位置在哪里?

    我已经为每个数据库表建立了一个模型,但我很想知道在何处以及如何实例化特定的模型方法。

    我一直在读的东西......

6 个答案:

答案 0 :(得分:6)

您已经在使用pub / sub模式,这似乎是合适的。除了上面的评论之外什么都没有,我会考虑将有序列表作为优先机制。

但是,每个订户都关注其家属的操作顺序以触发成功/失败,这仍然无法说明。依赖关系通常看起来像属于树,而不是列表。如果将它们存储在树中(使用复合模式),则内置递归将能够通过首先清理其依赖项来清除每个依赖项。这样你就不再担心优先处理清理发生的顺序 - 树会自动处理。

您可以使用树来存储pub / sub订阅者,就像使用列表一样容易。

使用测试驱动的开发方法可以为您提供所需的功能,并确保您的整个应用程序不仅可以完全测试,而且可以完全覆盖那些证明它能够满足您需求的测试。我首先要准确描述您需要做什么才能满足一个要求。

您知道要做的一件事是添加提供程序,因此TestAddProvider()测试似乎是合适的。请注意,此时它应该非常简单,并且与复合模式无关。一旦有效,您就知道提供者有依赖者。创建一个TestAddProviderWithDependent()测试,看看它是怎么回事。同样,它应该不复杂。接下来,您可能想要TestAddProviderWithTwoDependents(),这就是列表实现的地方。一旦它工作,你知道你希望Provider也是一个Dependent,所以一个新的测试将证明继承模型是有效的。从那里,您将添加足够的测试来说服自己添加提供者和家属的各种组合,以及异常条件的测试等。仅从测试和要求,您很快就会得到满足您需求的复合模式。在这一点上,我实际上打开了我的GoF副本,以确保我理解选择复合模式的后果,并确保我没有添加不适当的疣。

另一个已知的要求是删除提供程序,因此创建一个TestDeleteProvider()测试,并实现DeleteProvider()方法。您也不会让提供程序删除其依赖项,因此下一步可能是创建TestDeleteProviderWithADependent()测试。复合模式的递归在这一点上应该是显而易见的,你应该只需要更多的测试来说服自己,深层嵌套的提供者,空叶子,宽节点等都会正确地清理它们。

我认为您的提供商需要实际提供他们的服务。是时候测试调用提供者(使用模拟提供程序进行测试),并添加确保他们可以找到依赖关系的测试。同样,复合模式的递归应该有助于构建依赖关系列表或正确调用正确提供者所需的任何内容。

您可能会发现必须按特定顺序调用提供程序。此时,您可能需要为组合树中每个节点的列表添加优先级。或者您可能需要构建一个完全不同的结构(例如链表)以按正确的顺序调用它们。使用测试并慢慢接近它。您可能仍然有人担心您在特定的外部规定的订单中删除家属。此时,您可以使用您的测试向怀疑者证明您将永远安全地删除它们,即使它们不是按照他们想的顺序。

如果你做得对,你以前的所有测试都应该继续通过。

然后是棘手的问题。如果您有两个共享依赖的提供程序,该怎么办?如果删除一个提供程序,它是否应该删除所有依赖项,即使其他提供程序需要其中一个?添加测试,并实施您的规则。我想我会通过引用计数来处理它,但也许你想要第二个实例的提供者副本,所以你永远不必担心分享孩子,你就这样简单。或者它可能永远不会成为您域中的问题。另一个棘手的问题是,您的提供商是否可以拥有循环依赖关系。你如何确保自己最终没有自我参照循环?编写测试并弄清楚。

在您了解完整个结构之后,您才会开始考虑用于描述此层次结构的数据。

这是我考虑的方法。它可能不适合你,但那是由你来决定的。

答案 1 :(得分:4)

单元测试 通过单元测试,我们只想测试构成单个源代码单元的代码,通常是PHP中的类方法或函数(Unit Testing Overview)。这表明我们不想在单元测试中实际测试外部API,我们只想测试我们在本地编写的代码。如果您确实想要测试整个工作流程,那么您可能希望执行集成测试(Integration Testing Overview),这是一个不同的野兽。

当您特别询问有关单元测试的设计时,我们假设您实际上是指单元测试而不是集成测试,并提交有两种合理的方法来设计您的Provider类。

Stub Out 使用测试double替换对象(可选)返回已配置的返回值的做法称为存根。您可以使用存根来“替换SUT所依赖的实际组件,以便测试具有SUT的间接输入的控制点。这允许测试强制SUT向下路径,否则可能无法执行”。 Reference & Examples

模拟对象 用验证期望的测试double替换对象的做法,例如断言已调用方法,被称为模拟。

您可以使用模拟对象“作为观察点,用于在运行时验证SUT的间接输出。通常,模拟对象还包括测试存根的功能,因为它必须返回值SUT如果它还没有通过测试,但重点是验证间接输出。因此,模拟对象不仅仅是测试存根加断言;它使用了一种根本不同的方式“。 Reference & Examples

我们的建议 将你的课程设计为Stubbing和Mocking。 PHP单元手册有一个很好的example of Stubbing and Mocking Web Service。虽然这对开箱即用没有帮助,但它演示了如何为正在使用的Restful API实现相同的功能。

数据库查询的最佳位置在哪里? 我们建议您使用ORM而不是自己解决。您可以轻松地使用Google PHP ORM并根据自己的需求做出自己的决定; our advice is to use Doctrine因为我们使用Doctrine而且它很适合我们的需求,在过去的几年中,我们已经开始意识到Doctrine开发人员对域名的了解程度,简单地说,他们做得比我们自己做得更好,所以我们很高兴让他们为我们做。

如果您没有真正掌握使用ORM的原因,请参阅Why should you use an ORM?,然后再查看Google相同的问题。如果您仍然觉得自己可以使用自己的ORM或以其他方式自己处理数据库访问,那么我们希望您已经知道问题的答案。如果您觉得自己迫切需要自己处理,我们建议您查看一些ORM的源代码(See Doctrine on Github)并找到最适合您的方案的解决方案。

感谢您提出一个有趣的问题,我很感激。

答案 2 :(得分:2)

您的类层次结构中的每个依赖关系都必须可以从外部访问(不应该高度耦合)。例如,如果要在类B中实例化类A,则类B必须为类B中的类A实例持有者实现setter / getter方法。

http://en.wikipedia.org/wiki/Dependency_injection

答案 3 :(得分:2)

我可以用你的代码看到的另一个问题 - 这阻碍你实际测试它 - 正在使用静态类方法调用:

  • Provider::create('external::create-user')
  • $user = Provider_External::createUser()
  • $customer = Provider_Gapps_Customer::create($user)
  • $subscription = Provider_Gapps_Subscription::create($customer)
  • ...

它在您的代码中流行 - 即使您“仅”将它们概述为“简洁”的静态。这种优雅并不简洁,对于可测试的代码而言会产生反作用。在 所有费用 包含时避免使用这些内容。在询问有关单元测试的问题时,这是一种众所周知的不良做法,众所周知,此类代码难以测试

所有静态调用转换为对象方法调用并使用依赖注入而不是静态全局状态来传递对象后,您可以使用PHPUnit包进行单元测试。利用在(简单)测试中协作的存根和模拟对象。

所以这是一个TODO:

  1. 将静态方法调用重构为对象方法调用。
  2. 使用依赖注入传递对象。
  3. 您的代码非常完善。如果你认为你不能这样做,不要浪费你的时间进行单元测试,浪费它来维护你的应用程序,快速发货,让它赚钱,如果它不再有利可图就烧掉它。但是不要在单元测试静态全局状态时浪费你的编程生命 - 这只是愚蠢的事。

答案 4 :(得分:1)

考虑使用每个图层定义的角色和职责对应用程序进行分层。您可能希望从Apache-Axis' message flow subsystem中获取灵感。核心思想是创建一系列处理程序,请求在处理之前流动。这种设计便于可插入的部件,这些部件可以捆在一起以产生更高阶的功能。

此外,您可能希望阅读 Functors /功能对象,特别是关闭,谓词,变换器和供应商来创建您的参与组件。希望有所帮助。

答案 5 :(得分:0)

你看过州的设计模式了吗? http://en.wikipedia.org/wiki/State_pattern 您可以将所有步骤作为状态机中的不同状态,它看起来像图形。您可以将此图存储在数据库表/ xml中,每个提供程序也可以拥有自己的图表,表示执行的顺序。

因此,当您进入某种状态时,您可能会触发事件/事件(保存用户,获取用户)。我不知道您的应用程序特定,但事件可以被其他提供商重新使用。

如果某些步骤失败,则执行不同的图形路径。

如果你能正确地抽象它,你可以使用松散耦合的系统,它遵循图形给出的顺序并根据状态执行事件。

然后如果你需要添加其他提供者,你只需要创建图形和/或一些新事件。

以下是一些示例:https://github.com/Metabor/Statemachine