如何在 PHPUnit 中跨多个测试模拟测试 Web 服务?

Posted

技术标签:

【中文标题】如何在 PHPUnit 中跨多个测试模拟测试 Web 服务?【英文标题】:How to mock test a web service in PHPUnit across multiple tests? 【发布时间】:2011-01-28 08:11:44 【问题描述】:

我正在尝试使用 phpUnit 测试 Web 服务接口类。基本上,这个类调用 SoapClient 对象。我正在尝试使用此处描述的getMockFromWsdl 方法在 PHPUnit 中测试此类:

http://www.phpunit.de/manual/current/en/test-doubles.html#test-doubles.stubbing-and-mocking-web-services

但是,由于我想测试同一个类的多个方法,所以每次设置对象时,我还必须设置模拟 WSDL SoapClient 对象。这会导致抛出致命错误:

Fatal error: Cannot redeclare class xxxx in C:\web\php5\PEAR\PHPUnit\Framework\TestCase.php(1227) : eval()'d code on line 15

如何在多个测试中使用相同的模拟对象,而不必每次都从 WSDL 中重新生成它?这似乎是问题所在。

--

已被要求发布一些代码以查看,这是 TestCase 中的设置方法:

protected function setUp() 
    parent::setUp();

    $this->client = new Client();

    $this->SoapClient = $this->getMockFromWsdl(
        'service.wsdl'
    );

    $this->client->setClient($this->SoapClient);

【问题讨论】:

【参考方案1】:

对于基本用法,这样的东西会起作用。 PHPUnit 在幕后做了一些魔术。如果您缓存模拟对象,则不会重新声明它。只需从此缓存实例创建一个新副本,您就可以开始了。

<?php
protected function setUp() 
    parent::setUp();

    static $soapStub = null; // cache the mock object here (or anywhere else)
    if ($soapStub === null)
        $soapStub = $this->getMockFromWsdl('service.wsdl');

    $this->client = new Client;
    $this->client->setClient(clone $soapStub); // clone creates a new copy

?>

或者,您可以使用get_class 缓存类的名称,然后创建一个新实例,而不是副本。我不确定 PHPUnit 为初始化做了多少“魔法”,但值得一试。

<?php
protected function setUp() 
    parent::setUp();

    static $soapStubClass = null; // cache the mock object class' name
    if ($soapStubClass === null)
        $soapStubClass = get_class($this->getMockFromWsdl('service.wsdl'));

    $this->client = new Client;
    $this->client->setClient(new $soapStubClass);

?>

【讨论】:

我在问题中发布了一些代码以供审查。放入 class_exists 似乎并不能解决问题,因为您仍然必须以某种方式检索模拟对象,并且此类将始终运行该 eval。除非我要修改 PHPUnit 本身?您对 class_exists() 方法的放置位置有什么想法? 没有。停止。 PHPUnit 不需要 需要修改。严重地。你为什么使用评估?执行 eval 的类是问题所在。 'class_exists' 检查只是一个 hack 建议,以防止重新声明预先存在的类。这个检查需要放在执行“eval”的类中。 PHPUnit 调用的是 eval 代码,而不是我自己的代码。我认为它与解析 WSDL 并根据该定义创建模拟对象有关。 这个方法不太行。一旦你调用$this-&gt;client-&gt;getClient()-&gt;expects($this-&gt;any())-&gt;method('method')-&gt;will($this-&gt;returnValue($result));,任何后续测试也调用它,但结果不同,只会返回它遇到的第一个结果。猜测一下,结果是附加到类名的,所以它需要在调用之间是唯一的。【参考方案2】:

如果要在每次执行整个测试文件时获取一次模拟类定义,为什么要在 setUp() 中创建模拟?如果我没记错的话,它会在“this”测试类中定义的每个测试之前运行……试试 setUpBeforeClass()

来自http://www.phpunit.de/manual/3.4/en/fixtures.html

另外,setUpBeforeClass()和tearDownAfterClass()模板方法分别在测试用例类的第一个测试运行之前和测试用例类的最后一个测试运行之后调用。

【讨论】:

【参考方案3】:

PHPUnit 为基于 WSDL 的 mock 创建一个类。类名(如果未提供)由 .wsdl 文件名构成,因此始终相同。在整个测试中,当它尝试再次创建该类时,它会崩溃。

您唯一需要的是在 Mock 定义中添加您自己的类名,因此 PHPUnit 不会创建自动名称,请注意 $this->getMockFromWsdl 的第二个参数:

protected function setUp() 
   parent::setUp();

   $this->client = new Client();

   $this->SoapClient = $this->getMockFromWsdl(
      'service.wsdl', 'MyMockClass'
   );

   $this->client->setClient($this->SoapClient);

您现在可以创建任意数量的客户端,只需更改每个客户端的类名。

【讨论】:

这有同样的问题,在第二次测试中它会抱怨'MyMockClass'被重新声明。【参考方案4】:

这不是一个理想的解决方案,但在您的设置中为 SOAP 模拟提供一个“随机”类名,例如

$this->_soapClient = $this->getMockFromWsdl( 'some.wsdl', 'SoapClient' . md5( time().rand() ) );

这确保至少在调用设置时不会出现该错误。

【讨论】:

【参考方案5】:

在这里添加我的 $.02.. 我最近遇到了同样的情况,经过一些挫折之后,我能够解决它:

class MyTest extends PHPUnit_Framework_TestCase
    protected static $_soapMock = null;

    public function testDoWork_WillSucceed( )
    
        $this->_worker->setClient( self::getSoapClient( $this ) );
        $result = $this->_worker->doWork( );
        $this->assertEquals( true, $result['success'] );
    

    protected static function getSoapClient( $obj )
    
        if( !self::$_soapMock ) 
            self::$_soapMock = $obj->getMockFromWsdl( 
                'Test/wsdl.xml', 'SoapClient_MyWorker'
            );
        

        return self::$_soapMock;
    

我有很多“工人”,每个人都在自己的测试类中,每个人都需要访问模拟的 SOAP 对象。在 getMockFromWsdl 的第二个参数中,我必须确保我传递了一个唯一的名称(例如 SoapClient_MyWorker),否则它会导致 PHPUnit 崩溃。

我不确定是否从静态函数获取 SOAP 模拟并通过传入 $this 作为参数来访问 getMockFromWsdl 函数是实现此目的的最佳方法,但你可以。

【讨论】:

【参考方案6】:

在 PHPUnits 中将对象从测试传递到测试的一种方法是使用测试依赖项,如果实例化特定对象过于繁重/耗时:

<?php
/**
* Pass an object from test to test
*/
class WebSericeTest extends PHPUnit_Framework_TestCase


    protected function setUp() 
        parent::setUp();
        // I don't know enough about your test cases, and do not know
        // the implications of moving code out of your setup.
    

    /**
    * First Test which creates the SoapClient mock object.
    */
    public function test1()
    
        $this->client = new Client();

        $soapClient = $this->getMockFromWsdl(
            'service.wsdl'
        );

        $this->client->setClient($this->SoapClient);
        $this->markTestIncomplete();
        // To complete this test you could assert that the
        // soap client is set in the client object. Or
        // perform some other test of your choosing.

        return $soapClient;
    

    /**
    * Second Test depends on web service mock object from the first test.
    * @depends test1
    */
    public function test1( $soapClient )
    
        // you should now have the soap client returned from the first test.
        return $soapClient;
    

    /**
    * Third Test depends on web service mock object from the first test.
    * @depends test1
    */
    public function test3( $soapClient )
    
        // you should now have the soap client returned from the first test.
        return $soapClient;
    

?>

【讨论】:

以上是关于如何在 PHPUnit 中跨多个测试模拟测试 Web 服务?的主要内容,如果未能解决你的问题,请参考以下文章

PHPUnit:如何通过测试方法模拟内部调用的函数

在谷歌测试中跨多个文件分离测试用例

如何在 PHPUnit / Laravel 中模拟 JSON 文件

Phpunit 在测试类中仅模拟一种方法 - 使用 Mockery

Symfony 3模拟服务功能测试phpunit和liip

如何通过phpunit对一个方法进行单元测试,该方法具有多个内部调用保护/私有方法?