存根类调用的方法构造函数

发布于 2024-10-30 09:29:41 字数 1197 浏览 2 评论 0原文

如何存根 PHPUnit 中被测试类的构造函数调用的方法?例如,下面的简单代码将不起作用,因为当我声明存根方法时,存根对象已经创建,并且我的方法被调用,未存根。

测试类:

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

测试代码:

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

蓝天 2024-11-06 09:29:41

使用 disableOriginalConstructor() 以便 getMock() 不会调用构造函数。这个名称有点误导,因为调用该方法最终会为 $callOriginalConstructor 传递 false。这允许您在手动调用构造函数之前对返回的模拟设置期望。

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

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');
...
怎樣才叫好 2024-11-06 09:29:41

问题不在于方法的存根,而在于你的类。

您正在构造函数中进行工作。为了将对象设置为状态,您需要获取远程文件。但该步骤不是必需的,因为对象不需要该数据处于有效状态。在实际调用 getFormatted 之前,您不需要文件的结果。

您可以推迟加载:

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

这样您就可以在实际需要时延迟加载远程访问。现在您根本不需要存根getResultFromRemoteServer,而是可以存根getFormatted。您也不需要打开 API 进行测试,然后将 getResultFromRemoteServer 公开。

顺便说一句,即使它只是一个例子,我也会重写该类以读取

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

由于它是一个 Finder,所以现在实际上公开 findById 可能更有意义。只是对其进行保护,因为这就是您的示例中的内容。


另一个选项是扩展Subject-Under-Test,并将方法getResultFromRemoteServer替换为您自己的返回Poodle的实现代码>.这意味着您没有测试实际的 ClassA,而是测试 ClassA 的子类,但这就是您使用 Mock API 时发生的情况。

从 PHP7 开始,您可以使用这样的匿名类:

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

在 PHP7 之前,您只需编写一个扩展 Subject-Under-Test 的常规类,并使用它来代替 Subject-Under -测试。或者使用 disableOriginalConstructor,如本页其他地方所示。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文