如何在PHP单元测试中模拟构造函数中调用的方法?

时间:2014-04-15 08:55:32

标签: php unit-testing mocking phpunit mockery

我在使用构造函数中调用的方法对类进行单元测试时遇到了问题。我不明白如何嘲笑这个。也许我应该使用' setUp' phpUnit的方法?

我正在使用Mockery库。是否有比这更好的工具?

class ToTest
{

   function __construct() {

       $this->methodToMock(); // need to mock that for future tests 

   }

   // my methods class

}

任何建议都将不胜感激。

6 个答案:

答案 0 :(得分:5)

如果你的类很难实例化来测试,那就是你的类做得太多或在构造函数中工作的代码味道。

http://misko.hevery.com/code-reviewers-guide/

Flaw #1: Constructor does Real Work

警告标志

  • 构造函数或字段声明中的新关键字
  • 构造函数或字段声明中的静态方法调用
  • 除了构造函数中的字段赋值之外的任何内容
  • 构造函数完成后对象未完全初始化(观察 out for initialize methods)
  • 构造函数中的控制流(条件或循环逻辑)
  • 代码在构造函数中执行复杂的对象图构造 而不是使用工厂或建筑商
  • 添加或使用初始化块

无论您的methodToMock函数在构造函数中的作用是什么,都需要重新考虑。正如其他答案中所提到的,您可能希望使用依赖注入来传递您的类正在执行的操作。

重新思考你的班级实际上在做什么和重构,以便更容易测试。这样做的好处是可以让您的课程在以后更容易重复使用和修改。

答案 1 :(得分:2)

这里的问题是该方法不能被模拟,因为该对象尚未实例化。 sectus答案是有效的,但可能不是很灵活,因为在不同的测试中改变模拟方法的行为可能很困难。

您可以创建另一个与您要模拟的方法相同的类,并将该类的实例作为构造函数参数传递。这样你就可以在测试中传递一个模拟类。通常你遇到的问题是上课的气味太多了。

答案 2 :(得分:2)

要测试此类,您将模拟内部对象(methodToMock),然后使用依赖注入来传递模拟服务而不是真实服务。

类别:

class ToTest{
    private $svc;

    // Constructor Injection, pass the Service object here
    public function __construct($Service = NULL)
    {
        if(! is_null($Service) )
        {
            if($Service instanceof YourService)
            {
                $this->SetService($Service);
            }
        }
    }

    function SetService(YourService $Service)
    {
        $this->svc = $Service
    }

    function DoSomething($request) {
        $svc    = $this->svc;
        $result = $svc->getResult($request);        // Get Result from Real Service
        return $result;
    }

    function DoSomethingElse($Input) {
         // do stuff
         return $Input;
    }
}

测试:

class ServiceTest extends PHPUnit_Framework_TestCase
{
    // Simple test for DoSomethingElse to work Properly
    // Could also use dataProvider to send different returnValues, and then check with Asserts.
    public function testDoSomethingElse()
    {
        $TestClass = new YourService();
        $this->assertEquals(1, $TestClass->DoSomethingElse(1));
        $this->assertEquals(2, $TestClass->DoSomethingElse(2));
    }

    public function testDoSomething()
    {
        // Create a mock for the YourService class,
        // only mock the DoSomething() method. Calling DoSomethingElse() will not be processed
        $MockService = $this->getMock('YourService', array('DoSomething'));

        // Set up the expectation for the DoSomething() method 
        $MockService->expects($this->any())
                    ->method('getResult')
                    ->will($this->returnValue('One'));

        // Create Test Object - Pass our Mock as the service
        $TestClass = new ToTest($MockService);
        // Or
        // $TestClass = new ToTest();
        // $TestClass->SetService($MockService);

        // Test DoSomething
        $RequestString = 'Some String since we did not specify it to the Mock';  // Could be checked with the Mock functions
        $this->assertEquals('One', $TestClass->DoSomething($RequestString));
    }
}

答案 3 :(得分:1)

我也想知道这就是我找到你问题的方式。最后我决定做一些有点脏的事情......用反射。

以下是我要测试的方法:

/**
 * ArrayPool constructor.
 * @param array $tasks Things that might be tasks
 */
public function __construct(array $tasks)
{
    foreach ($tasks as $name => $parameters) {
        if ($parameters instanceof TaskInterface) {
            $this->addTask($parameters);
            continue;
        }
        if ($parameters instanceof DescriptionInterface) {
            $this->addTask(new Task($parameters));
            continue;
        }
        $this->addPotentialTask($name, $parameters);
    }
}

出于本次测试的目的,我不想实际运行->addTask->addPotentialTask,只知道它们会被调用。

以下是测试:

/**
 * @test
 * @covers ::__construct
 * @uses \Foundry\Masonry\Core\Task::__construct
 */
public function testConstruct()
{
    $task = $this->getMockForAbstractClass(TaskInterface::class);
    $description = $this->getMockForAbstractClass(DescriptionInterface::class);
    $namedTask = 'someTask';
    $parameters = [];

    $arrayPool =
        $this
            ->getMockBuilder(ArrayPool::class)
            ->disableOriginalConstructor()
            ->setMethods(['addTask', 'addPotentialTask'])
            ->getMock();

    $arrayPool
        ->expects($this->at(0))
        ->method('addTask')
        ->with($task);
    $arrayPool
        ->expects($this->at(1))
        ->method('addTask')
        ->with($this->isInstanceOf(TaskInterface::class));
    $arrayPool
        ->expects($this->at(2))
        ->method('addPotentialTask')
        ->with($namedTask, $parameters);

    $construct = $this->getObjectMethod($arrayPool, '__construct');
    $construct([
        0=>$task,
        1=>$description,
        $namedTask => $parameters
    ]);
}

魔术发生在getObjectMethod中,它接受一个对象并返回一个可调用的闭包,该闭包将在该对象的一个​​实例上调用该方法:

/**
 * Gets returns a proxy for any method of an object, regardless of scope
 * @param object $object Any object
 * @param string $methodName The name of the method you want to proxy
 * @return \Closure
 */
protected function getObjectMethod($object, $methodName)
{
    if (!is_object($object)) {
        throw new \InvalidArgumentException('Can not get method of non object');
    }
    $reflectionMethod = new \ReflectionMethod($object, $methodName);
    $reflectionMethod->setAccessible(true);
    return function () use ($object, $reflectionMethod) {
        return $reflectionMethod->invokeArgs($object, func_get_args());
    };
}

我知道循环和条件都能正常运行而不需要进入代码我不想进入这里。

enter image description here

TL; DR:

  1. Disble __construct
  2. 设置模拟
  3. 在实例化对象后使用反射来调用__construct
  4. 尽量不要失去任何睡眠

答案 4 :(得分:0)

只需扩展此类并覆盖您公开或受保护的方法。

答案 5 :(得分:0)

class ToTest
{
   function __construct(){
       $this->methodToMock(); // need to mock that for future tests 
   }
   // my methods class
    public function methodToMock(){}
}

class ToTestTest{
    /**
     * @test
     * it should do something
     */
    public function it_should_do_something(){
        $ToTest = \Mockery::mock('ToTest')
        ->shouldDeferMissing()
        ->shouldReceive("methodToMock")
        ->andReturn("someStub")
        ->getMock();

        $this->assertEquals($expectation, $ToTest->methodToMock());
    }
}