测试替身

9 测试替身

PHPUnit 提供的 createMock($type)getMockBuilder($type) 方法可以在测试中用来自动生成对象,此对象可以充当任意指定原版类型(接口或类名)的测试替身。在任何预期或要求使用原版类的实例对象的上下文中都可以使用这个测试替身对象来代替。

createMock($type) 方法直接返回指定类型(接口或类)的测试替身对象实例。此测试替身的创建使用了最佳实践的默认值(不执行原始类的 __construct()__clone() 方法,且不对传递给测试替身的方法的参数进行克隆)。如果这些默认值非你所需,可以用 getMockBuilder($type) 方法并使用流畅式接口来定制测试替身的生成过程。

在默认情况下,原版类的所有方法都会被替换为只会返回 null 的伪实现(其中不会调用原版方法)。使用诸如 will($this->returnValue()) 之类的方法可以对这些伪实现在被调用时应当返回什么值做出配置。

简单来说就是一些测试类需要调用其他的类作为参数,但是我们又不想创建那些类,所以PHPUnit提供了一种模仿机制。

桩件:Stubs

将对象替换成配置好对应返回值的替身的方法成为上桩(stubbing)

设置返回值

可以返回的数据种类有:

  • 固定值
  • 传入的参数
  • 返回桩件类自身
  • 根据传入的参数,返回不同的值
  • 设置回调函数,计算返回值
  • 设置同一方法的多次调用时,每次的返回值
  • 设置抛出指定异常

下面是demo

require_once __DIR__."/../../vendor/autoload.php";

use PHPUnit\Framework\TestCase;

class SomeClass
{
    public function doSomething()
    {
        // 随便做点什么。
        return "data";
    }
    public function callUndefinedFunction($param){
        return "你不可以调用在原始类中未定义的方法";
    }
    public function classTest(){
        return "self";
    }
    public function returnValueByParam(){

    }
    public function returnDataByCallBack(){

    }
    public function returnDataByList(){

    }
    public function throwExceptions(){

    }
}

class Mock extends TestCase{
    public function testExample(){

        $stub = $this->createMock(SomeClass::class);
        // 返回固定值
        $stub->method('doSomething')
            ->willReturn('foo');
        // 返回参数
        $stub->method("callUndefinedFunction")
            ->will($this->returnArgument(0));
        // 返回该桩类自身
        $stub->method("classTest")
            ->will($this->returnSelf());
        // 根据不同参数,返回不同的值
        $map=array(
            // 最后一个值永远是返回的结果,而前面的就是传递的参数
            array("param1","param2","param3",'result1'),
            array('param2','param3','result2'),
            array("result3")
        );
        $stub->method("returnValueByParam")
            ->will($this->returnValueMap($map));
        // 下面调用时传入多少个参数,都会自动传入这里。
        // 但是,下面调用的参数只能比这里多,不能少,否则会报错误
        $callBackFunction=function ($param1,$param2,$param3){
            print_r(array(
                $param1,
                $param2,
                $param3
            ));
        };
        $stub->method("returnDataByCallBack")
            ->will($this->returnCallback($callBackFunction));
        // 设置每一次调用的返回值
        $stub->method("returnDataByList")
            ->will($this->onConsecutiveCalls(1,2,3,4,5,6));// 注意,这里传入的不是数组!
        // 设置代码抛出异常
        $stub->method("throwExceptions")
            ->will($this->throwException(new \Exception("没事情,就是抛异常玩玩")));
        $this->assertEquals('foo', $stub->doSomething());
        $this->assertEquals("hello",$stub->callUndefinedFunction("hello"));
        $this->assertSame($stub,$stub->classTest());
        $this->assertEquals("result1",$stub->returnValueByParam("param1","param2","param3"));
        $this->assertEquals("result2",$stub->returnValueByParam('param2','param3'));
        $this->assertEquals("result3",$stub->returnValueByParam());
        // 这里调用上面设置的方法,其中的参数会自动传入回调函数中
//        $stub->returnDataByCallBack(1,2,3,4);
        $this->assertEquals(1,$stub->returnDataByList());
        $this->assertEquals(2,$stub->returnDataByList());
        try{
            $stub->throwExceptions();
        }catch (Exception $e){
            print "\r\n".$e->getMessage()."\r\n";
        }
    }
}

注意,不能使用$stub->method()设置对应类中不存在的方法,否则会报错。

创建桩件时进行限制

public function testStubSetting(){
        $stub=$this->getMockBuilder(SomeClassSecond::class)
                    ->disableOriginalClone()// 开启设置之后,原类的 __clone 函数就不会运行了
                    ->disableOriginalConstructor()// 开启这个之后,就不会运行原类的 __construct 方法了
                    ->disableArgumentCloning()// 开启这个时候就不会在创建桩件的时候,复制原类的属性
                    ->disallowMockingUnknownTypes()// 不允许定义不存在的属性
                    ->getMock();
        $stub2= clone $stub;// 即使上面限制了 disableOriginalClone,但是那只是限制不能运行原类的 __clone,clone函数还是生效的,所以下面的断言是正确的
        $this->assertEquals($stub2,$stub,"两个类不一致");
    }

仿件对象:Mock Object

仿件对象有桩件的功能,但是比桩件更加贴合原来的类。举个例子就是桩件只能调用设置好的方法,不能调用原类的一些方法,但是仿件对象可以。

创建仿件对象


require_once __DIR__."/../../vendor/autoload.php";

use PHPUnit\Framework\TestCase;

class Subject
{
    protected $observers = [];
    protected $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function doSomething()
    {
        // 做点什么
        // ...

        // 通知观察者发生了些什么
        $this->notify('something');
    }

    public function doSomethingBad()
    {
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }

    protected function notify($argument)
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }

    // 其他方法。
}

class Observer
{
    public function update($argument)
    {
        // 做点什么。
        print "\r\n\{$argument}r\n";
    }

    public function reportError($errorCode, $errorMessage, Subject $subject)
    {
        // 做点什么。
    }
    public function selfFunction(){
        print "\r\nSelfFunction\r\n";
    }
    // 其他方法。
}

class MockObjectTest extends TestCase{
    public function testCopy(){
        // 为 Observer 类建立仿件对象,只模仿 update() 方法。
        $observer = $this->getMockBuilder(Observer::class)
            ->setMethods(['update'])
            ->getMock();

        // 建立预期状况:update() 方法将会被调用一次。只能调用一次,不能多不能少
        // 并且将以字符串 'something' 为参数,不能以其他字符串作为参数
        $observer->expects($this->once())
            ->method('update')
            ->with($this->equalTo('something'));// 设置参数一定是 something
				$observer->selfFunction();// 这里只是尝试调用原类的方法
        // 创建 Subject 对象,并将模仿的 Observer 对象连接其上。
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // 在 $subject 对象上调用 doSomething() 方法,
        // 预期将以字符串 'something' 为参数调用
        // Observer 仿件对象的 update() 方法。
        $subject->doSomething();
    }
}

注意这里的一些细节:

  • 我们创建的仿件对象覆盖对应类的一个或者多个方法:setMethods($methods=array())
  • 我们可以设置对应覆盖方法调用一定要调用一次:$this->once()
  • 我们可以设置预期参数:$this->equalTo()
  • 我们可以将仿件对象就当作原类来使用,即使加了强制类型限制也可以通过

with 方法解释

with() 方法可以携带任何数量的参数,对应于被模仿的方法的参数数量。可以对方法的参数指定更加高等的约束而不仅是简单的匹配。

public function testErrorReported()
    {
        // 为 Observer 类建立仿件,对 reportError() 方法进行模仿
        $observer = $this->getMockBuilder(Observer::class)
            ->setMethods(['reportError'])
            ->getMock();

        $observer->expects($this->once())
            ->method('reportError')
            ->with(
                $this->greaterThan(0),
                $this->stringContains('Something'),
                $this->anything()
            );

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() 方法应当会通过(observer的)reportError()方法
        // 向 observer 报告错误。
        $subject->doSomethingBad();
    }

withConsecutive 方法解释

withConsecutive() 方法可以接受任意多个数组作为参数,具体数量取决于欲测试的调用。每个数组都都是对被仿方法的相应参数的一组约束,就像 with() 中那样。

这个单词分开解释就是连续的with,简单来说就是设置该方法多次调用时,设置每次的预期。

public function testFunctionCalledTwoTimesWithSpecificArguments()
    {
        $mock = $this->getMockBuilder(stdClass::class)
            ->setMethods(['set'])
            ->getMock();

        $mock->expects($this->exactly(2))
            ->method('set')
            // consecutive 英文翻译 连续的
            ->withConsecutive(
                [$this->equalTo('foo'), $this->greaterThan(0)],
                [$this->equalTo('bar'), $this->greaterThan(0)]
            );

        $mock->set('foo', 21);
        $mock->set('bar', 48);
    }

上面我们设置了一个方法只调用一次用了$this->once(),如果我们要设置一个方法被调用N次,则需要使用$this->exactly()方法。

with的拓展

这个语言很难解释,但是看代码就可以理解了:

public function testErrorReported()
    {
        // 为 Observer 类建立仿件,模仿 reportError() 方法
        $observer = $this->getMockBuilder(Observer::class)
            ->setMethods(['reportError'])
            ->getMock();

        $observer->expects($this->once())
            ->method('reportError')
            ->with($this->greaterThan(0),
                $this->stringContains('Something'),
                // 上面将对应原本本身传入
                $this->callback(function($subject){
                    return is_callable([$subject, 'getName']) &&
                        $subject->getName() == 'My subject';
                }));

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() 方法应当会通过(observer的)reportError()方法
        // 向 observer 报告错误。
        $subject->doSomethingBad();
    }
    public function testIdenticalObjectPassed()
    {
        $expectedObject = new stdClass;

        $mock = $this->getMockBuilder(stdClass::class)
            ->setMethods(['foo'])
            ->getMock();

        $mock->expects($this->once())
            ->method('foo')
            // 预期参数将与 stdClass 一样
            ->with($this->identicalTo($expectedObject));

        $mock->foo($expectedObject);
    }

调用次数函数汇总

  • any:任意次数
  • never:一次也不行
  • atLeastOnce:至少一次
  • once:只有一次
  • exactly:指定次数exactly(int $count)
  • at:指定第几次,第一次下标为0,at(int $index)

getMockBuilder

  • setMethods(array $methods):设置覆盖的方法
  • setConstructorArgs(array $args):向原类的__constructor方法传递参数
  • disableOriginalConstructor:禁用原类的__constructor方法
  • disableOriginalClone:禁用原类的__clone方法

Prophecy

这玩意是另一个测试框架,有机会学习吧。(下辈子吧)

对trait和abstract的类进行模拟

针对trait来说,就是$this->getMockForTrait(AbstractTrait::class);,剩下的就是一样的了。

getMockForAbstractClass() 方法返回一个抽象类的仿件对象。给定抽象类的所有抽象方法将都被模仿。这样就能对抽象类的具体方法进行测试。

require_once __DIR__."/../../vendor/autoload.php";

use PHPUnit\Framework\TestCase;

trait AbstractTrait
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class classTest extends TestCase{
    public function testConcreteMethod()
    {
        $mock = $this->getMockForTrait(AbstractTrait::class);

        $mock->expects($this->any())
            ->method('abstractMethod')
            ->will($this->returnValue(true));

        $this->assertTrue($mock->concreteMethod());
    }
}

针对abstract来说也是一样:$this->getMockForAbstractClass(AbstractClass::class);

require_once __DIR__."/../../vendor/autoload.php";

use PHPUnit\Framework\TestCase;

abstract class AbstractClass
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class classTest extends TestCase{
    public function testConcreteMethod()
    {
        $stub = $this->getMockForAbstractClass(AbstractClass::class);

        $stub->expects($this->any())
            ->method('abstractMethod')
            ->will($this->returnValue(true));

        $this->assertTrue($stub->concreteMethod());
    }
}

对web服务进行上桩或模仿

拓展知识点太多了,以后有机会再说吧。

wsdl介绍

这里贴一下我找到的例子中的GoogleSearch.wsdl的文件内容:







<definitions name="GoogleSearch"
             targetNamespace="urn:GoogleSearch"
             xmlns:typens="urn:GoogleSearch"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
             xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
             xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
             xmlns="http://schemas.xmlsoap.org/wsdl/">

    

    <types>
        <xsd:schema xmlns="http://www.w3.org/2001/XMLSchema"
                    targetNamespace="urn:GoogleSearch">

            <xsd:complexType name="GoogleSearchResult">
                <xsd:all>
                    <xsd:element name="documentFiltering"           type="xsd:boolean"/>
                    <xsd:element name="searchComments"              type="xsd:string"/>
                    <xsd:element name="estimatedTotalResultsCount"  type="xsd:int"/>
                    <xsd:element name="estimateIsExact"             type="xsd:boolean"/>
                    <xsd:element name="resultElements"              type="typens:ResultElementArray"/>
                    <xsd:element name="searchQuery"                 type="xsd:string"/>
                    <xsd:element name="startIndex"                  type="xsd:int"/>
                    <xsd:element name="endIndex"                    type="xsd:int"/>
                    <xsd:element name="searchTips"                  type="xsd:string"/>
                    <xsd:element name="directoryCategories"         type="typens:DirectoryCategoryArray"/>
                    <xsd:element name="searchTime"                  type="xsd:double"/>
                xsd:all>
            xsd:complexType>

            <xsd:complexType name="ResultElement">
                <xsd:all>
                    <xsd:element name="summary" type="xsd:string"/>
                    <xsd:element name="URL" type="xsd:string"/>
                    <xsd:element name="snippet" type="xsd:string"/>
                    <xsd:element name="title" type="xsd:string"/>
                    <xsd:element name="cachedSize" type="xsd:string"/>
                    <xsd:element name="relatedInformationPresent" type="xsd:boolean"/>
                    <xsd:element name="hostName" type="xsd:string"/>
                    <xsd:element name="directoryCategory" type="typens:DirectoryCategory"/>
                    <xsd:element name="directoryTitle" type="xsd:string"/>
                xsd:all>
            xsd:complexType>

            <xsd:complexType name="ResultElementArray">
                <xsd:complexContent>
                    <xsd:restriction base="soapenc:Array">
                        <xsd:attribute ref="soapenc:arrayType" wsdl:arrayType="typens:ResultElement[]"/>
                    xsd:restriction>
                xsd:complexContent>
            xsd:complexType>

            <xsd:complexType name="DirectoryCategoryArray">
                <xsd:complexContent>
                    <xsd:restriction base="soapenc:Array">
                        <xsd:attribute ref="soapenc:arrayType" wsdl:arrayType="typens:DirectoryCategory[]"/>
                    xsd:restriction>
                xsd:complexContent>
            xsd:complexType>

            <xsd:complexType name="DirectoryCategory">
                <xsd:all>
                    <xsd:element name="fullViewableName" type="xsd:string"/>
                    <xsd:element name="specialEncoding" type="xsd:string"/>
                xsd:all>
            xsd:complexType>

        xsd:schema>
    types>

    

    <message name="doGetCachedPage">
        <part name="key"            type="xsd:string"/>
        <part name="url"            type="xsd:string"/>
    message>

    <message name="doGetCachedPageResponse">
        <part name="return"         type="xsd:base64Binary"/>
    message>

    <message name="doSpellingSuggestion">
        <part name="key"            type="xsd:string"/>
        <part name="phrase"         type="xsd:string"/>
    message>

    <message name="doSpellingSuggestionResponse">
        <part name="return"         type="xsd:string"/>
    message>

    

    <message name="doGoogleSearch">
        <part name="key"            type="xsd:string"/>
        <part name="q"              type="xsd:string"/>
        <part name="start"          type="xsd:int"/>
        <part name="maxResults"     type="xsd:int"/>
        <part name="filter"         type="xsd:boolean"/>
        <part name="restrict"       type="xsd:string"/>
        <part name="safeSearch"     type="xsd:boolean"/>
        <part name="lr"             type="xsd:string"/>
        <part name="ie"             type="xsd:string"/>
        <part name="oe"             type="xsd:string"/>
    message>

    <message name="doGoogleSearchResponse">
        <part name="return"         type="typens:GoogleSearchResult"/>
    message>

    

    <portType name="GoogleSearchPort">

        <operation name="doGetCachedPage">
            <input message="typens:doGetCachedPage"/>
            <output message="typens:doGetCachedPageResponse"/>
        operation>

        <operation name="doSpellingSuggestion">
            <input message="typens:doSpellingSuggestion"/>
            <output message="typens:doSpellingSuggestionResponse"/>
        operation>

        <operation name="doGoogleSearch">
            <input message="typens:doGoogleSearch"/>
            <output message="typens:doGoogleSearchResponse"/>
        operation>

    portType>


    

    <binding name="GoogleSearchBinding" type="typens:GoogleSearchPort">
        <soap:binding style="rpc"
                      transport="http://schemas.xmlsoap.org/soap/http"/>

        <operation name="doGetCachedPage">
            <soap:operation soapAction="urn:GoogleSearchAction"/>
            <input>
                <soap:body use="encoded"
                           namespace="urn:GoogleSearch"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            input>
            <output>
                <soap:body use="encoded"
                           namespace="urn:GoogleSearch"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            output>
        operation>

        <operation name="doSpellingSuggestion">
            <soap:operation soapAction="urn:GoogleSearchAction"/>
            <input>
                <soap:body use="encoded"
                           namespace="urn:GoogleSearch"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            input>
            <output>
                <soap:body use="encoded"
                           namespace="urn:GoogleSearch"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            output>
        operation>

        <operation name="doGoogleSearch">
            <soap:operation soapAction="urn:GoogleSearchAction"/>
            <input>
                <soap:body use="encoded"
                           namespace="urn:GoogleSearch"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            input>
            <output>
                <soap:body use="encoded"
                           namespace="urn:GoogleSearch"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            output>
        operation>
    binding>

    
    <service name="GoogleSearchService">
        <port name="GoogleSearchPort" binding="typens:GoogleSearchBinding">
            <soap:address location="http://api.google.com/search/beta2"/>
        port>
    service>

definitions>

对文件系统进行模拟

安装依赖

composer require mikey179/vfsStream

示例1

示例1主要演示的就是没有使用文件测试前,测试文件相关时的例子。

require_once __DIR__."/../../vendor/autoload.php";

use PHPUnit\Framework\TestCase;

class Example
{
    protected $id;
    protected $directory;

    public function __construct($id)
    {
        $this->id = $id;
    }

    public function setDirectory($directory)
    {
        $this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;

        if (!file_exists($this->directory)) {
            mkdir($this->directory, 0700, true);
        }
    }
}

class ExampleTest extends TestCase
{
    protected function setUp():void
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(file_exists(dirname(__FILE__) . '/id'));

        $example->setDirectory(dirname(__FILE__));
        $this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
    }

    protected function tearDown():void
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }
}

这个麻烦就麻烦在文件创建完之后要写代码清除,如果清除不干净,则会对下次测试造成影响。比如在这里我们就使用了setUptearDown,来清除文件。

示例2

require_once __DIR__."/../../vendor/autoload.php";

use PHPUnit\Framework\TestCase;

class Example
{
    protected $id;
    protected $directory;

    public function __construct($id)
    {
        $this->id = $id;
    }

    public function setDirectory($directory)
    {
        $this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;

        if (!file_exists($this->directory)) {
            mkdir($this->directory, 0700, true);
        }
    }
}

class ExampleTest extends TestCase
{
    public function setUp():void
    {
        \org\bovigo\vfs\vfsStreamWrapper::register();
        \org\bovigo\vfs\vfsStreamWrapper::setRoot(new \org\bovigo\vfs\vfsStreamDirectory('exampleDir'));

    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(\org\bovigo\vfs\vfsStreamWrapper::getRoot()->hasChild('id'));

        $example->setDirectory(\org\bovigo\vfs\vfsStream::url('exampleDir'));
        $this->assertTrue(\org\bovigo\vfs\vfsStreamWrapper::getRoot()->hasChild('id'));
    }
}

关于mikey179/vfsStream这个拓展类的更多介绍,这里我就不深究了,以后用到再说吧。

你可能感兴趣的:(php)