开发者

Stubbing a method called by a class' constructor

How does one stub a method in PHPUnit that is called by the class under test's constructor? The simple code below for example won't work because by the time I declare the stubbed method, the stub object has already been created and my method called, unstubbed.

Class to test:

class ClassA {
  private $dog;
  private $formatted;

  public function __construct($param1) { 
     $this->dog = $param1;       
     $this->getResultFromRemoteServer();
  }

  // Would normally be private, made public for stubbing
  public getResultFromRemoteServer() {
    $this->formatted = file_get_contents('http://whatever.com/index.php?'.$this->dog);
  }

  public getFormatted() {
    return ("The dog is a ".$this->formatted);
  }
}

Test code:

class ClassATest extends PHPUnit_Framework_TestCase {
  public function testPoodle() {  
    $stub = $this->getMockBuilder('ClassA')
                 ->setMethods(array('getResultFromRemoteServer'))
           开发者_运维百科      ->setConstructorArgs(array('dog52'))
                 ->getMock();

    $stub->expects($this->any())
         ->method('getResultFromRemoteServer')
         ->will($this->returnValue('Poodle'));

    $expected = 'This dog is a Poodle';
    $actual = $stub->getFormatted();
    $this->assertEquals($expected, $actual);
  }
}


Use disableOriginalConstructor() so that getMock() won't call the constructor. The name is a bit misleading because calling that method ends up passing false for $callOriginalConstructor. This allows you to set expectations on the returned mock before calling the constructor manually.

$stub = $this->getMockBuilder('ClassA')
             ->setMethods(array('getResultFromRemoteServer'))
             ->disableOriginalConstructor()
             ->getMock();
$stub->expects($this->any())
     ->method('getResultFromRemoteServer')
     ->will($this->returnValue('Poodle'));
$stub->__construct('dog52');
...


The problem is not the stubbing of the method, but your class.

You are doing work in the constructor. In order to set the object into state, you fetch a remote file. But that step is not necessary, because the object doesn't need that data to be in a valid state. You dont need the result from the file before you actually call getFormatted.

You could defer loading:

class ClassA {
  private $dog;
  private $formatted;

  public function __construct($param1) { 
     $this->dog = $param1;       
  }
  protected getResultFromRemoteServer() {
    if (!$this->formatted) {
        $this->formatted = file_get_contents(
            'http://whatever.com/index.php?' . $this->dog
        );
    }
    return $this->formatted;
  }
  public getFormatted() {
    return ("The dog is a " . $this->getResultFromRemoteServer());
  }
}

so you are lazy loading the remote access to when it's actually needed. Now you dont need to stub getResultFromRemoteServer at all, but can stub getFormatted instead. You also won't need to open your API for the testing and make getResultFromRemoteServer public then.

On a sidenote, even if it's just an example, I would rewrite that class to read

class DogFinder
{
    protected $lookupUri;
    protected $cache = array();
    public function __construct($lookupUri)
    {
        $this->lookupUri = $lookupUri;
    }
    protected function findById($dog)
    {
        if (!isset($this->cache[$dog])) {
            $this->cache[$dog] = file_get_contents(
                urlencode($this->lookupUri . $dog)
            );
        }
        return $this->cache[$id];
    }
    public function getFormatted($dog, $format = 'This is a %s')
    {
        return sprintf($format, $this->findById($dog));
    }
}

Since it's a Finder, it might make more sense to actually have findById public now. Just keeping it protected because that's what you had in your example.


The other option would be to extend the Subject-Under-Test and replace the method getResultFromRemoteServer with your own implementation returning Poodle. This would mean you are not testing the actual ClassA, but a subclass of ClassA, but this is what happens when you use the Mock API anyway.

As of PHP7, you could utilize an Anonymous class like this:

public function testPoodle() {

    $stub = new class('dog52') extends ClassA {
      public function getResultFromRemoteServer() {
          return 'Poodle';
      }
    };

    $expected = 'This dog is a Poodle';
    $actual = $stub->getFormatted();
    $this->assertEquals($expected, $actual);
}

Before PHP7, you'd just write a regular class extending the Subject-Under-Test and use that instead of the Subject-Under-Test. Or use disableOriginalConstructor as shown elsewhere on this page.

0

上一篇:

下一篇:

精彩评论

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

最新问答

问答排行榜