解耦或嘲笑?

时间:2015-09-03 09:11:34

标签: php unit-testing phpunit

假设我有这门课程:

class SomeClass
{
    // Top level function
    public function execute($command)
    {
        // Get output from system tool
        $output = $this->runTool($command);

        // Check output for errors
        if ($this->hasError($output))
            return false;

        // And parse success response from tool
        return $this->parseOutput($output);
    }

    // There we're make a call to system
    private function runTool($command)
    {
        return `/some/system/tool $command`;
    }
    [...]
}

我不想在我的测试中运行系统工具,我想用预定义的输出替换系统调用。

所以,问题是 - 我应该创建另一个类,在其中移动系统调用并在测试中模拟该类,或者我只能模拟我将要测试的类的函数?

当然,这两种方法都可行,但是哪种方法可以更好地为测试目的服务呢?

3 个答案:

答案 0 :(得分:2)

如果您遵循单一责任原则,您就不会遇到此问题。您的课程不需要知道如何进行系统调用,因此您必须使用另一个类。你嘲笑那个。

IMO,在大多数情况下,当你需要模拟受保护的或私有的方法时,他们会做一些应该进入另一个类并被嘲笑的东西。

答案 1 :(得分:0)

我会说这实际上取决于您的基础设施。有时最好使用Mock,有时候使用Stub。

如果是这样的话,你要测试的类包含这个不需要的方法 - 使用Mock并仅模拟这一个函数。这将使您确信,对该类所做的任何更改都将由测试处理。

如果不需要的函数是注入服务或另一个类的一部分,而不是此特定测试的域,则可以创建存根。

答案 2 :(得分:0)

您无法测试私有方法,您可以使用变通方法并按照此article中所述通过反射调用它,并在此SO QUESTION中讨论

但我建议您将方法可见性更改为protected并仅模拟runTool方法的行为。

例如,假设您的类的以下修改版本(我不知道其他方法如何工作,所以我想您要测试他们的行为并以此实现为例):

<?php


namespace Acme\DemoBundle\Service;


    class SomeClass
    {
        // Top level function
        public function execute($command)
        {
            // Get output from system tool
            $output = $this->runTool($command);

            // Check output for errors
            if ($this->hasError($output))
                return false;

            // And parse success response from tool
            return $this->parseOutput($output);
        }

        // There we're make a call to system
        protected function runTool($command)
        {
            return `/some/system/tool $command`;
        }

        private function hasError($output)
        {
            return $output == "error";
        }

        private function parseOutput($output)
        {
            return json_decode($output);
        }

    }

假设以下测试用例:

<?php

namespace Acme\DemoBundle\Tests;


class SomeClassTest extends \PHPUnit_Framework_TestCase {

    public function testCommandReturnError()
    {
        $mock = $this->getMockBuilder('Acme\DemoBundle\Service\SomeClass')
            ->setMethods(array('runTool'))
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $mock
        ->expects($this->exactly(1))
            ->method('runTool')
            ->with("commandName")
            ->will($this->returnValue("error"));

        $this->assertFalse($mock->execute("commandName"));
    }

    public function testCommandReturnCorrectValue()
    {
        $mock = $this->getMockBuilder('Acme\DemoBundle\Service\SomeClass')
            ->setMethods(array('runTool'))
            ->disableOriginalConstructor()
            ->getMock()
        ;

        $mock
            ->expects($this->exactly(1))
            ->method('runTool')
            ->with("commandName")
            ->will($this->returnValue('{"title":"myTitle"}'));

        $returnValue = $mock->execute("commandName");
        $this->assertEquals("myTitle", $returnValue->title);
    }

}

希望这个帮助