【问题标题】:How to test a Factory / Strategy implementation with PHPUnit如何使用 PHPUnit 测试工厂/策略实现
【发布时间】:2013-01-08 22:45:52
【问题描述】:

我有一个工厂类,它根据给定文件的扩展名返回写入策略:

public static function getWriterForFile($file)
{
    // create file info object
    $fileInfo = new \SplFileInfo($file);
    // check that an extension is present
    if ('' === $extension = $fileInfo->getExtension()) {
        throw new \RuntimeException(
            'No extension found in target file: ' . $file
        );
    }
    // build a class name using the file extension
    $className = 'MyNamespace\Writer\Strategy\\'
        . ucfirst(strtolower($extension))
        . 'Writer';
    // attempt to get an instance of the class
    if (!in_array($className, get_declared_classes())) {
        throw new \RuntimeException(
            'No writer could be found for the extension: ' . $extension
        );
    }
    $instance = new $className();
    $instance->setTargetFile($file);
    return $instance;
}

我所有的策略都实现了相同的接口,例如:

class CsvWriter implements WriterStrategyInterface
{
    public function setTargetFile($file)
    {
        ...
    }

    public function writeLine(array $data)
    {
        ...
    }

    public function flush()
    {
        ...
    }
}

我希望能够使用虚拟扩展来测试此方法,以便我的测试不依赖于任何现有的特定策略。我尝试使用设置的类名为接口创建一个模拟,但这似乎没有声明模拟类名:

public function testExpectedWriterStrategyReturned()
{
    $mockWriter = $this->getMock(
        'MyNamespace\Writer\Strategy\WriterStrategyInterface',
        array(),
        array(),
        'SssWriter'
    );

    $file = 'extension.sss';

    $writer = MyNamespace\Writer\WriterFactory::getWriterForFile($file);

    $this->assertInstanceOf('MyNamespace\Writer\Strategy\WriterStrategyInterface', $writer);;
}

我有什么方法可以为工厂加载模拟编写器策略,或者我应该重构工厂方法以使其更具可测试性?

【问题讨论】:

    标签: php mocking phpunit factory strategy-pattern


    【解决方案1】:

    该工厂的主要目的和职责是创建对象。

    恕我直言,您应该使用

    进行测试
    $this->assertInstanceOf('class', $factory->getWriterForFile($file));
    

    为什么不进一步抽象呢?

    您可以以某种方式实现,以便工厂将对象创建分派给另一个类。

    拥有一个“对象创建者”:

    $class = new ReflectionClass($class);
    $instance = $class->newInstanceArgs($args);
    

    或类似的东西,但是除了“测试看起来更像是一个单元测试”之外,您还有什么收获?你并没有真正改进你的代码库,只是为了测试能力而进行的更改对我来说总是有点可笑。

    我会将工厂测试视为集成/接线测试,以确保您的所有工厂实际工作并生产所需的对象。


    对于您当前的代码示例,我建议的唯一更改是更改两件事:

    a) 使工厂方法非静态

    如果你没有很好的理由,你不会从静态工厂获得任何东西,除非你不能注入它并使其成为静态使其可以从全局范围访问,因此将依赖项放入工厂甚至需要更多的全局变量等等。如果你对工厂进行 DI,通常使它们成为合适的对象也有助于避免未来的问题

    b) 不要在工厂里做diskIo。使用:getWriterForFile(\SplFileInfo $file)

    工厂不应该关心文件系统。如果它需要一个现有文件,它应该需要一个并将如何处理错误的详细信息留给消费者。

    这给您带来的好处是您还可以传递SplTempFileObject,这将使测试更容易,并且即使出于生产目的,您的工厂也可以独立于文件系统。

    【讨论】:

    • 感谢您的建议,我已经在讨论静态问题了,所以就这样达成了交易。不过,我很清楚,您认为在测试工厂时使用代码库中的真正编写器策略是否可以,而不是试图让工厂生成模拟对象?我担心这会使测试依赖于它未测试的类,但我不确定这是否重要?
    • 是的,我建议使用实物。工厂的预期功能是创建真实对象和工厂线对象图。所以他们也可以做真实的事情,你可以从测试它们中获得好处,这比仅仅测试 string.concat 是否正确要好。它不再是纯粹的单元测试,但考虑到替代方案,我会说它更有用。
    • 公平地说,我想最坏的情况是,如果我使用的策略被删除,那么测试将失败,直到它被替换。我可以忍受。
    • @edorian 但是您也可以使用静态方法进行 DI。为什么需要实例化?
    猜你喜欢
    • 1970-01-01
    • 2019-07-26
    • 1970-01-01
    • 1970-01-01
    • 2012-01-26
    • 2023-03-11
    • 2013-12-25
    • 2011-12-14
    • 2017-06-16
    相关资源
    最近更新 更多