【问题标题】:What is the alternative to making static method calls in unrelated classes?在不相关的类中进行静态方法调用的替代方法是什么?
【发布时间】:2011-07-22 12:56:55
【问题描述】:

我正在使用 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 中是这样,因为你只能模拟......基本上只有一层。

我订购了《有效地使用遗留代码》一书,但是有哪些策略可以开始重构和管理此类代码以实现可测试性?

【问题讨论】:

    标签: php oop architecture phpunit static-libraries


    【解决方案1】:

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

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

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

    您定义了一个接口(我们现在称之为 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

    【讨论】:

    • 由于我提供了我正在尝试重构的代码类型的代码示例,您认为您可以提供修复该代码的基本代码示例吗?
    • 另请注意,我将您原来的 set_timezone() 方法更改为不再是静态的,因为如果它保持这种状态,那么您使用 core_lang 的其他类将以相同的方式耦合到它因为cfg 耦合到core_lang。综上所述,您还应该为core_lang 定义一个接口,以便在需要测试时能够模拟(或切换实现)core_lang
    【解决方案2】:

    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()
            );
        }
    }
    

    它还允许通过Test Helpers extension 使用Stubbing HardCoded Dependencies

    注意:the Test-Helper extension is supersededhttps://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。因此,这使您能够真正开始改进遗留代码。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-09
      • 2010-12-25
      • 1970-01-01
      • 1970-01-01
      • 2013-08-08
      相关资源
      最近更新 更多