使用依赖注入进行基本初始化的可测试(小)应用程序

时间:2011-05-13 09:11:08

标签: php unit-testing phpunit

越来越多地采用适当的方法以可测试的方式编写代码,过去两周彻底在从头开始创建一个小应用程序时改变了我的方法。

由于我经常需要编写小型php控制台应用程序,我目前的目标是使用最小的应用程序方法以可测试的方式初始化一个小应用程序。只需要通常的配置类,使用正确使用的单例进行数据库连接,错误处理配置和日志记录。

我已经用很多不同的方式解决了这个问题,但是现在我不知道我是否正在朝着正确的方向前进。阅读关于依赖注入的帖子,我开始尝试创建具有对象作为参数的对象,同时遵守Demeter的规律以便于模拟。我已经消除了不是真正常量的全局常量。我刚刚删除了大多数静态调用,并且对于所需的单例,例如app-wide数据库连接,尝试应用Mr Hevery's suggestions并将其包含/包装在一个对象中,该对象将与协作类中的其他所需对象一起传递。

这是一个非常简单的例子,直到我从头到目前为止拼凑起来。我正在研究的方法是使用一个AppFactoryHelper类,它充当一个非常简单的工厂并创建上面提到的对象。

我创建了一个配置对象,它只是一个带有getter和setter的常规数组,一个DB单例连接,一个logger对象,以及整个工厂的error_handling类,即:

// very sketchy outline of programm flow for initialisation, stripped of error handling 

require_once 'includes/settings.php';
require_once 'AppHelperFactory.php';
require_once 'container.php';

// object to load in container
$appObjects = array = ('log', 'db', 'error_handling');

$appHelperFactory = new AppHelperFactory;

//config object will be needed helper objects
$config = $appHelperFactory->createConfig($settings.php); // 

// create dependency container, sets config object as private property inside,
// container holds only getters for $config.
$container = $appHelperFactory->createContainer($config);
try{      
  foreach($appObject as $className){
     $methodName = 'create' . $className; 
     $container->{$value} = $appHelperFactory->{$methodName}($config);
 }


$app = new ObjectThatWillFinallyGetSomethingDone($container);

$app->doStuff();

这种方法虽然提出了问题。我是否使用依赖注入甚至接近我应该采取的正确方式? DB单例在容器中更好吗?在测试中一直困扰我的事情,我将如何测试我的“主”文件?

2 个答案:

答案 0 :(得分:1)

我发布后很快就看到了你的问题,但我没有一个非常可靠的答案方法。

所以我决定从上到下调整你的帖子并回答所有引起我兴趣的事情。

  

与正确使用的单身人士的数据库连接

Singletons have no use in PHP

如果你只需要一个,只需要一个。如果你使用单例,它只会使测试变得更难并引入全局状态。

  

我刚刚删除了大多数静态调用,以及所需的单例,例如app-wide数据库连接

再次:“单身人士”,如'我只创造一个就好了。像“模式”中的“单身人士”不是。只是不要,你不需要它。

  

包含/包装在一个对象中,该对象将与协作类中的其他所需对象一起传递。

所以基本上是一个注册表?你把东西放进来,你的应用程序希望它可以从中提取东西。所以你要传递一个中间对象而不是真实对象。它可以工作,但通常不是最好的方法。

Google Tech Talk:The Clean Code Talks - Don't Look For Things!在解释原因方面做得很好。

  

我正在研究的方法是使用一个AppFactoryHelper类,它充当一个非常简单的工厂,并创建上面提到的对象。

你只需要在你的bootstrap中为你的应用程序创建一个基本对象,所以我不能确定你为什么要将它们包装到工厂中但是很好。在谈到代码时我会回过头来看。

现在代码

首先让我说,我尝试阅读那些~30行5次,直到我得到了你想要做的事情,即使代码有点短。可能只是我;)

要求:

现在通常使用自动加载器,如果你不喜欢通过将你的类名绑在一起来削弱你的文件结构,那就使用类似phpab的东西来保证灵活性。

对象:

您使用AppHelperFactory抽象应用程序所做的一切。

$appHelperFactory = new AppHelperFactory;
$config = $appHelperFactory->createConfig($settings.php); 
// dunno what $settings.php means here, i assume you mean "settings.php" or something

我只是建议

$config = new Config(); 
$config->readFromFile("settings.php"); 

也可以完成工作,因为我假设你不需要在整个地方创建配置对象,你不应该为了它而建立一个工厂。

您的其他对象也是如此。

你的ObjectThatWillFinallyGetSomethingDone依赖于一个容器,它希望至少拥有4个对象,并且模拟那些用于测试的容器将非常痛苦。 (或者至少比它需要更多的痛苦)。

特别是当你创建一个Web应用程序时,我没有看到有一个“应用程序”对象(如果你使用它作为你的“主要方法”,我觉得很好),但如果你想要它真的单元可测试你我需要传递比现在更多的对象。像路由器,一些“controllerFactory”(或者你处理调度到执行业务逻辑的代码的请求)等等。

  

我是否使用依赖注入甚至接近我应该采用的正确方式?

您正在使用注册表。那是别的但是相关的。

阅读该主题(赞成和反对):

Flaw: Brittle Global State & Singletons (“添加或使用注册表”和“添加或使用服务定位器”部分)

Do you need a Dependency Injection Container?

Dealing with dependencies(只是一些样本)

并且有点相关Object lifecycle control


你所展示的所有代码都集中在“构建大量内容并将其置于某个地方,以防我需要它”,并且没有显示您实际尝试做的事情(通常是回答请求)。

所以对于那个..是啊..你设法做到这一点,除了一些小的代码投诉

 $container->{$value} = $appHelperFactory->{$methodName}($config); // really? :(

你有点成功。如果那些实际上有帮助或任何好处都不能仅仅从那段引导中说出来。

希望有所帮助..你也可以跳到这里的PHP聊天中进行那些讨论,如果你因为太主观而无法进入问题;)

答案 1 :(得分:0)

为什么不简单地使用符合条件的Symfony2或其他框架?它支持DI,控制台应用程序,日志记录,数据库连接(使用Doctrine DBAL或ORM)等。它也是由数百人编写和测试的,所以它更加安全和灵活。

  1. 数据库连接不应该是单例。可能有多个连接实例。此外,不需要全局的应用程序访问,因为此对象仅由负责维护业务数据的某些服务使用。
  2. 使用reflection代替$container->{$value}。处理更多“静态”代码要容易得多。

  3. 编辑,2011-05-16

    首先,当我写道:

      

    可能有多个连接实例。

    我的意思是您可以与多个数据库建立多个连接。创建与单个数据库的多个连接毫无意义。

    应将数据库连接视为服务。假设你正在使用PDO所以你需要做的就是定义几个连接参数并创建一个服务(伪代码):

    ...
    <parameters>
       <parameter key="db.connection.dns">mysql:dbname=testdb;host=127.0.0.1</parameter>
       <parameter key="db.connection.username">root</parameter>
       <parameter key="db.connection.password">password</parameter>
       <parameter key="db.connection.options" type="collection" />
    </parameters>
    ...
    <services>
       <service id="db.connection" class="PDO">
         <argument>%db.connection.dns%</argument>
         <argument>%db.connection.username%</argument>
         <argument>%db.connection.password%</argument>
         <argument type="collection">%db.connection.options%</argument>
       </service>
    </services>
    ...
    

    现在,每当您请求容器返回db.connection服务($container->getService('db.connection');)时,它将在必要时创建并返回对连接对象的引用。

    假设您有两个需要数据库连接的服务user.managerthread.manager。只需在容器中定义对该连接的引用:

    <service id="user.manager" class="UserManager">
      <argument type="service">db.connection</argument>
    </service>
    
    <service id="thread.manager" class="...">
      <argument type="service">db.connection</argument>
    </service>
    
    class UserManager {
      public function __construct(PDO $conn) {
         ...
      }
    }
    

    这就是全部。单个对象将通过所有需要它的服务传递。

    您可以在ZF项目中使用Symfony的DIC。它可以用作独立组件:

    1. http://symfony.com/doc/current/book/service_container.html
    2. http://components.symfony-project.org/dependency-injection/documentation
    3. https://github.com/symfony/symfony/tree/master/src/Symfony/Component/DependencyInjection