【问题标题】:Unit testing legacy PHP class with heavy DB dependency对具有严重 DB 依赖性的遗留 PHP 类进行单元测试
【发布时间】:2012-10-05 11:15:52
【问题描述】:

我正在尝试在旧版 PHP 应用程序中实现一些单元测试。

这方面存在许多挑战,但特别是对于这个问题,我目前正在研究一个管理应用配置的小类。

类接口非常简单;它执行以下操作:

  • 构造函数调用 Populate 方法,该方法使用我们的 Recordset 类从数据库加载所请求模块的配置。
  • Get 方法,返回指定的配置值。
  • 设置方法,设置内存中的配置值。
  • Save 方法,将配置更新写回数据库。

测试 Get/Set 方法很简单;它们直接映射到私有数组,并且几乎可以按照您的预期工作。

我遇到的问题是测试数据库处理。该类使用配置表上的许多字段(模块名称、语言等)来确定要加载哪些配置项以及优先级。为此,它构造了一系列复杂的 SQL 字符串,然后直接调用 DB 以获取正确的配置数据。

我不知道如何为此编写单元测试。除了 Get/Set 方法之外,该类几乎完全由构建 SQL 字符串和运行它们组成。

如果不针对真正的数据库实际运行它,我看不到一种合理测试它的方法,以及随之而来的所有问题——如果不出意外的话,配置加载器的复杂性意味着我需要至少七个或八个测试数据库填充了稍微不同的配置。看起来它会变得难以管理和脆弱,这会在某种程度上破坏这一点。

谁能建议我应该如何从这里开始?甚至可以对这种类进行单元测试吗?

非常感谢。

【问题讨论】:

    标签: php unit-testing dependencies legacy-code


    【解决方案1】:

    更好地测试这些东西的一个直接方法是为您的配置类提供一个访问数据库的对象,这样您就可以使用真实数据库运行任何配置,或者只是一些模拟它写入内存,甚至是轻量级文件,如果例如,您需要持久性。

    这可以通过创建具有定义接口的适配器来完成。您编写的第一个适配器用于您的数据库。

    当配置对象被创建时,你传入适配器。由于它具有定义的接口,因此配置类可以与具有该接口的任何适配器一起使用。首先是数据库。

    然后你要么模拟一个适配器,要么为你的测试编写一个它自己的适配器。在您的测试中,您不使用数据库适配器,而是使用测试适配器。

    然后您可以独立于数据库对配置类进行单元测试。

    【讨论】:

    • 感谢您的回复。我想问题的一部分是类构建的查询实际上是功能所在。模拟数据库意味着我们不运行查询。如果不运行查询,测试类确实变得更容易,但也有点毫无意义。查询使用 MySQL 方言,因此可能不适用于 sqllite 表,因此也无法使用。 :(
    【解决方案2】:

    我必须说我不确定我是否同意如果不在这里访问数据库,单元测试会有些毫无意义。我的目标是在不涉及您的数据库的情况下获得生成被测 SQL 的业务逻辑。这是我所说的一个例子:

    class Foo {
    
        // ... Getters and setters for your config ...
    
        public function doSomeBusinessLogicThenHitDb()
        {
            $sql = 'SELECT * FROM mytable WHERE ';
            $sql .= $this->_doSomethingComplicatedThatInvolvesParsingTheConfig();
            $this->_queryDb($sql);
        }
    
        protected function _queryDb($sql)
        {
            // Do something with a PDO or whatever
        }
    }
    

    _queryDb() 位抽象为一个单独的函数后,您可以编写以下测试:

        public function testMyClassUnderSomeCircumstances()
        {
            // Set up config
            $exampleConfig = // whatever
    
            // Set up expected result
            $whatTheSqlShouldLookLikeForThisConfig = 'SELECT ... WHERE ...';
    
            // Set up a partial mock that doesn't actually hit the DB
            $myPartialMockObject = $this->getMock('Foo', array('_queryDb'), array(), '');
            $myPartialMockObject->expects($this->once())
                                ->method('_queryDb')
                                ->with($whatTheSqlShouldLookLikeForThisConfig);
    
            // Exercise the class under test
            $myPartialMockObject->setConfig($exampleConfig);
            $myPartialMockObject->doSomeBusinessLogicThenHitTheDb();
        }
    

    此测试的重点是测试生成您的 SQL 的业务逻辑 - 而不是测试您的数据库本身。通过期望生成的 SQL 必须看起来像它必须看起来的样子,您可以确保如果对 _doSomethingComplicatedThatInvolvesParsingTheConfig() 的无辜重构意外地破坏了您的代码,您的测试将失败,因为它会产生与过去不同的 SQL .

    如果您的目标是测试整个应用程序(包括其数据库),请尝试使用合适的集成测试套件,例如 Selenium。单元测试监视各个类并告诉您它们何时停止按应有的行为。如果您让它们超出范围,您将面临执行速度和错误本地化的问题(即错误甚至在代码中,更不用说被测试的类了,还是数据库问题?)。

    【讨论】:

    • 感谢您的回答。考虑到问题的年龄,出乎意料。 :-) 自发布问题以来,我最终进行了相当大规模的重构练习,主要是为了使所有代码进入可测试状态。那是不久前的事了,一切都很顺利。但是你给出的建议是好的;我会给你+1并接受答案。感谢您努力提供帮助。
    猜你喜欢
    • 2014-03-15
    • 2013-05-10
    • 1970-01-01
    • 2017-09-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多