开发者

What is the alternative to making static method calls in unrelated classes?

I'm unit testing and refactoring a large code base with many utility libraries in PHP.

There's many libraries like this, filled with convenience methods used all over the site. Most of these static libraries interact with the configuration files (via another static class). Here's a good example:

class core_lang {
    public static function set_timezone()
    {
        if(cfg::exists('time_zone')) {
            putenv("TZ=".cfg:开发者_运维问答:get('time_zone'));
        }
    }
}

Then, of course, there's another layer of more specific libraries elsewhere calling core_lang:: set_timezone() inside another function.

That's making these classes VERY hard to write unit tests for, at least in PHPUnit, since you can only mock... basically one level in.

I ordered the book Working Effectively with Legacy Code, but what are some strategies for starting to refactor and manage this type of code for testability?


The most important principle to reduce coupling is Dependency Injection. There are many methods to actually implement it, but the base concept is the same:

Don't hardcode dependencies into your code, ask for them instead.

In your particular example, one method to do this right is the following:

You define an interface (let's call it ExistenceChecker for now) which exposes a method called 'exists()'. In production code, you create a class which actually implements the method (let's call it ConcreteExistenceChecker), and you ask for an ExistenceChecker object in the constructor of core_lang. This way you can pass a stub object which implement this interface (but with a dead simple trivial implementation) while you unit test your code. From now on, you don't have to depend on a concrete class, just an interface, which introduces far less coupling.

Let me demonstrate it with a bit of code:

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

Production code:

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

Test code:

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

You can read more about this concept here.


The author of PHPUnit has a blog post about Stubbing and Mocking Static Methods. It generally advises the same as that other answers, namely dont use statics because they are death to testability, but change the code to use Dependency Injection.

However, PHPUnit does allow for mocking and stubbing static method calls.

Example from BlogPost for Stubbing Static methods:

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

and it also allows for Stubbing HardCoded Dependencies via the Test Helpers extension.

Note: the Test-Helper extension is superseded by https://github.com/krakjoe/uopz

Example from BlogPost for Stubbing Hardcoded Dependencies:

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

Testing your code this way doesnt mean it's fine to use hardcoded static dependencies. You should still refactor the code to use Dependency Injection. But in order to refactor you have to have UnitTests first. So this enables you to actually start improving the legacy code.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜