在不相关的类中进行静态方法调用的替代方法是什么?

时间:2011-03-24 22:46:25

标签: php oop architecture phpunit static-libraries

我正在使用PHP中的许多实用程序库对大型代码库进行单元测试和重构。

有很多这样的图书馆,充满了遍布整个网站的便捷方法。大多数静态库与配置文件交互(通过另一个静态类)。这是一个很好的例子:

class core_lang {
    public static function set_timezone()
    {
        if(cfg::exists('time_zone')) {
            putenv("TZ=".cfg::get('time_zone'));
        }
    }
}

然后,当然,在另一个函数中调用core_lang:: set_timezone()的地方还有另一层更具体的库。

这使得这些类非常难以编写单元测试,至少在PHPUnit中,因为你只能模拟...基本上是一个级别。

我订购了“有效使用旧版代码”一书,但是有哪些策略可以开始重构和管理这类代码以实现可测试性?

2 个答案:

答案 0 :(得分:3)

减少耦合的最重要原则是依赖注入。有很多方法可以实际实现它,但基本概念是相同的:

不要将依赖项硬编码到您的代码中,而是要求它们。

在您的特定示例中,执行此操作的一种方法如下:

您定义了一个接口(我们现在将其称为ExistenceChecker),它公开了一个名为“exists()”的方法。在生产代码中,您创建了一个实际实现该方法的类(让我们称之为ConcreteExistenceChecker),并在core_lang的构造函数中请求ExistenceChecker对象。这样,您可以在单元测试代码时传递实现此接口的存根对象(但具有简单的简单实现)。从现在开始,您不必依赖于具体的类,只需要一个接口,这会引入更少的耦合。

让我用一些代码演示它:

interface ExistenceChecker {
    public function exists($timezone);
}

class ConcreteExistenceChecker implements ExistenceChecker {
    public function exists($timezone) {
        // do something and return a value
    }
}

class ExistenceCheckerStub implements ExistenceChecker {
    public function exists($timezone) {
        return true; // trivial implementation for testing purposes
    }
}

class core_lang {    
    public function set_timezone(ExistenceChecker $ec)
    {
        if($ec->exists('time_zone')) {
            putenv("TZ=".cfg::get('time_zone'));
        }
    }
}

生产代码:

// setting timezone
$cl = new core_lang();
$cl->set_timezone(new ConcreteExistenceChecker()); // this will do the real work

测试代码:

// setting timezone
$cl = new core_lang();
$cl->set_timezone(new ExistenceCheckerStub()); // this will do the mocked stuff

您可以详细了解此概念here

答案 1 :(得分:3)

PHPUnit的作者有一篇关于Stubbing and Mocking Static Methods的博客文章。它通常建议与其他答案相同,即dont use statics,因为它们是death to testability,但更改代码以使用依赖注入。

但是,PHPUnit 确实允许模拟和存根静态方法调用。

来自BlogPost的示例用于存根静态方法:

class FooTest extends PHPUnit_Framework_TestCase
{
    public function testDoSomething()
    {
        $class = $this->getMockClass(
          'Foo',          /* name of class to mock     */
          array('helper') /* list of methods to mock   */
        );

        $class::staticExpects($this->any())
              ->method('helper')
              ->will($this->returnValue('bar'));

        $this->assertEquals(
          'bar',
          $class::doSomething()
        );
    }
}

并且还允许Stubbing HardCoded Dependencies通过Test Helpers extension

  

注意: the Test-Helper extension is superseded https://github.com/krakjoe/uopz

BlogPost中用于存根硬编码依赖项的示例:

class FooTest extends PHPUnit_Framework_TestCase
{
    protected function setUp()
    {
        $this->getMock(
          'Bar',                    /* name of class to mock     */
          array('doSomethingElse'), /* list of methods to mock   */
          array(),                  /* constructor arguments     */
          'BarMock'                 /* name for mocked class     */
        );

        set_new_overload(array($this, 'newCallback'));
    }

    protected function tearDown()
    {
        unset_new_overload();
    }

    protected function newCallback($className)
    {
        switch ($className) {
            case 'Bar': return 'BarMock';
            default:    return $className;
        }
    }

    public function testDoSomething()
    {
        $foo = new Foo;
        $this->assertTrue($foo->doSomething());
    }
}

以这种方式测试代码并不意味着使用硬编码的静态依赖项是很好的。您仍然应该重构代码以使用依赖注入。但是为了重构你必须首先拥有UnitTests。因此,这使您能够真正开始改进遗留代码。