• 【PHP】PHPUnit单元测试利器:PHP Mock的使用方法


    由于环境依赖关系,或者是特殊环境的构造要求,这就可能导致我们在测试环境下做验证是很困难的。

    当我们无法直接使用的真实被依赖模块时,我们可以用“测试替身”(Test Double)来代替。这个测试替身不需要与真实的被依赖模块有相同的行为,它只需要提供和真实的被依赖模块有相同的API就行了。

    PHPUnit提供的getMock($className)方法可以自动生成一个对象,而这个对象就可以作为原来那个类的测试替身。这个测试替身可以用在任何需要它的地方。

    默认情况下,原类的所有方法都被一个虚拟的实现替代,这个实现仅仅是返回NULL(不会调用原类中的对应方法)。你可以使用will($this->returnValue())方法来配置其被调用时的返回值,从而将这些虚拟的实现具体化。

    限制:final,privatestatic方法不能被插桩或者模拟,在测试替身中,这些方法会保留其原有的实现。

    警告:需要注意的是其参数管理已经被修改了。原先的实现是拷贝所有的参数,这样就无法判断传到函数里的对象是否是相同的了。例10.14显示了新的实现所带来的好处,例10.15显示了如何切换回到以前的行为(见本文最后)

    打桩就是使用测试替身对象来替换原有的对象,而这个测试替身的返回值是可配置的。你可以使用桩来替换测试所依赖的真实模块,这样就可以在测试的间接输入中得到一个控制点,这样就可以让测试流程不要再继续执行下去,因为不这样的话测试可能无法正常执行下去。

    10.2显示如何对方法调用进行打桩以及如何设置该方法的返回值。我们首先使用PHPUnit_Framework_TestCase类所提供的getMock()方法来建立一个stub对象,这个对象就如例10.1中的SomeClass对象一样。 然后,我们使用PHPUnit提供的一系列接口来指定桩的行为。 从本质上讲,这意味着你不需要创建多个临时对象,并把它们绑在一起。 相反,使用示例中所示链式的方法调用将导致代码更易读。

    例 10.1: 待插桩的类

    <?Php
    class SomeClass {
        public function doSomething() {
            // Do something.
        }
    }
    ?>

    例 10.2: 对函数调用进行插桩并指定返回值

    <?php
    require_once 'SomeClass.php';
    
    class StubTest extends PHPUnit_Framework_TestCase {
    
        public function testStub() {
            // Create a stub for the SomeClass class. 
            $stub = $this->getMock('SomeClass');
            
            // Configure the stub. 
            $stub->expects($this->any())
                 ->method('doSomething')
                 ->will($this->returnValue('foo'));
            
            // Calling $stub->doSomething() will now return 
            // 'foo'. 
            $this->assertEquals('foo', $stub->doSomething());
        }
    }
    ?> 

    在以上代码中,当我们调用getMock()方法时,PHPUnit会自动生成一个新的类,并且通过这个类来实现预期的行为。这个测试替身类是可以通过可选参数来配置的。

    • 默认情况下,除非是通过will($this->returnValue())来指定了返回值,否则都是返回NULL
    • 当第二个参数(可选)被指定时,表明只有array数组里配置的方法才会被替换,其他方法依然保留原有的实现。
    • 第三个参数(可选)可以指定一个参数数组并传给原类的构造函数(构造函数在默认情况下是不会被虚拟实现替换掉的)。
    • 第四个参数(可选)可以为测试替身类指定一个类名。
    • 第五个参数(可选)可用于禁用调用原类的构造函数 。
    • 第六个参数(可选)可用于禁用原类的复制构造函数的调用 。
    • 第七个参数(可选)可以用于禁止测试替身类生成过程中对__autoload()的调用。

    另外一个可选择的方法是使用Mock Builder API来配置生成的测试替身类。如例10.3.以下列出Mock Builder提供的接口:

    • setMethods(array $methods)可以用于Mock Builder对象需要替换的方法,其他方法仍然保留原类的实现。
    • 调用setConstructorArgs(array $args)方法可以指定传入原类的构造函数的参数(原类的构造函数在默认情况下是不会被虚拟化的)。
    • setMockClassName($name)可用于指定测试替身类的类名。
    • disableOriginalConstructor()可用于禁调用原类的构造函数。
    • disableOriginalClone()可以用来禁止原类的复制构造函数的调用。
    • disableAutoload()可以用于禁止生成测试替身类时__autoload()的调用。

    例 10.3: 用Mock Builder API来配置生成的测试替身类

    <?php
    require_once 'SomeClass.php';
    
    class StubTest extends PHPUnit_Framework_TestCase {
    
        public function testStub() {
            // Create a stub for the SomeClass class. 
            $stub = $this->getMockBuilder('SomeClass')->disableOriginalConstructor()->getMock();
            
            // Configure the stub. 
            $stub->expects($this->any())
                 ->method('doSomething')
                 ->will($this->returnValue('foo'));
            
            // Calling $stub->doSomething() will now return 
            // 'foo'. 
            $this->assertEquals('foo', $stub->doSomething());
        }
    }
    ?> 

    有时你可能希望被打桩的方法的返回值是某个传入的参数,这时可以使用例10.4中所示的通过替换resutnValue()returnArgument()方法来实现:

    例 10.4: 对指定方法打桩,并让其返回指定传入参数

    <?php
    require_once 'SomeClass.php';
    
    class StubTest extends PHPUnit_Framework_TestCase {
    
        public function testReturnArgumentStub() {
            // Create a stub for the SomeClass class. 
            $stub = $this->getMock('SomeClass');
            
            // Configure the stub. 
            $stub->expects($this->any())
                 ->method('doSomething')
                 ->will($this->returnArgument(0));
            
            // $stub->doSomething('foo') returns 'foo' 
            $this->assertEquals('foo', $stub->doSomething('foo'));
            
            // $stub->doSomething('bar') returns 'bar' 
            $this->assertEquals('bar', $stub->doSomething('bar'));
        }
    }
    ?> 

    有时需要被打桩的方法返回被打桩的类的引用,这时可以使用returnSelf()方法来实现,如例10.5所示:

    例 10.5: 对指定方法打桩并使其返回打桩对象本身

    <?php
    require_once 'SomeClass.php';
    
    class StubTest extends PHPUnit_Framework_TestCase {
    
        public function testReturnSelf() {
            // Create a stub for the SomeClass class. 
            $stub = $this->getMock('SomeClass');
            
            // Configure the stub. 
            $stub->expects($this->any())
                 ->method('doSomething')
                 ->will($this->returnSelf());
            
            // $stub->doSomething() returns $stub 
            $this->assertSame($stub, $stub->doSomething());
        }
    }
    ?> 

    有时被打桩的方法需要针对不同的参数返回不同的值,这时可以使用returnValueMap()创建一个map来关联参数和返回值。如例10.6所示:

    例 10.6: 对指定方法打桩,并使其返回值为map里配置的值

    <?php
    require_once 'SomeClass.php';
    
    class StubTest extends PHPUnit_Framework_TestCase {
    
        public function testReturnValueMapStub() {
            // Create a stub for the SomeClass class. 
            $stub = $this->getMock('SomeClass');
            
            // Create a map of arguments to return values. 
            $map = array(
                array('a', 'b', 'c', 'd'),
                array('e', 'f', 'g', 'h')
            );
            
            // Configure the stub. 
            $stub->expects($this->any())
                 ->method('doSomething')
                 ->will($this->returnValueMap($map));
            
            // $stub->doSomething() returns different values depending on 
            // the provided arguments. 
            $this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
            $this->assertEquals('h', $stub->doSomething('e', 'f', 'g'));
        }
    }
    ?> 

    当被打桩的方法需要返回一个计算值,而不是固定值(参见returnValue())或者指定参数(参见returnArgument)时,这时可以使用returnCallback()来指定被打桩方法的回调函数。如例10.7

    例 10.7: 对指定方法打桩,并使其返回值为指定函数调用的返回值

    <?php
    require_once 'SomeClass.php';
    
    class StubTest extends PHPUnit_Framework_TestCase {
    
        public function testReturnCallbackStub() {
            // Create a stub for the SomeClass class. 
            $stub = $this->getMock('SomeClass');
            
            // Configure the stub. 
            $stub->expects($this->any())
                 ->method('doSomething')
                 ->will($this->returnCallback('str_rot13'));
            
            // $stub->doSomething($argument) returns str_rot13($argument) 
            $this->assertEquals('fbzrguvat', $stub->doSomething('something'));
        }
    }
    ?> 

    另外一种更简单的设置回调函数的方法是:设定期望返回值列表。你可以使用onConsecutiveCalls()来实现这个功能,如例10.8所示:

    例 10.8: 对指定方法打桩,并使其返回值按指定序列逐次返回

    <?php
    require_once 'SomeClass.php';
    
    class StubTest extends PHPUnit_Framework_TestCase {
    
        public function testOnConsecutiveCallsStub() {
            // Create a stub for the SomeClass class. 
            $stub = $this->getMock('SomeClass');
            
            // Configure the stub. 
            $stub->expects($this->any())
                 ->method('doSomething')
                 ->will($this->onConsecutiveCalls(2, 3, 5, 7));
            
            // $stub->doSomething() returns a different value each time 
            $this->assertEquals(2, $stub->doSomething());
            $this->assertEquals(3, $stub->doSomething());
            $this->assertEquals(5, $stub->doSomething());
        }
    }
    ?>

    抛出异常(略)

    Mock对象

    Mocking是指使用测试替身来替换原有对象并对期望作检查,例如断言某个方法被调用了。

    你可以使用mock对象作为观察点来验证测试过程中的间接输出。通常来说,mock对象包含测试桩的功能,因此它必须要在测试过程中有返回值(如何测试未失败的话),但更重要的是对于间接输出的验证。因此,mock对象远不止是一个测试桩加一个断言这么简单,它是用完全不同的方式。

    注意:要mock的类如果不存在的话,phpunit会生成一个空的同名的类。如果要使用原来的类的话,需要把声明该类的文件包含进来,不然的话就可能会提示"Fatal error:Call to undefined method XXX::xxx() in xxx.php on line xxx"这类错误了。

    以下是一个例子,假设我们需要测试测试例子中的update()方法,这个方法是被另外一个对象的观察者调用的,如例10.10

    例 10.10: 类Subject和Observer都是测试系统的一部分

    <?php
    class Subject {
        protected $observers = array();
    
        public function attach(Observer $observer) {
            $this->observers[] = $observer;
        }
    
        public function doSomething() {
            // Do something. 
            // ... 
            
            // Notify observers that we did something. 
            $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);
            }
        }
    
        // Other methods. 
    }
    
    class Observer {
    
        public function update($argument) {
            // Do something. 
        }
    
        public function reportError($errorCode, $errorMessage, Subject $subject) {
            // Do something 
        }
    
        // Other methods. 
    }
    ?> 

    10.11显示了如何使用mock对象来测试SubjectObserver对象之间的相互作用:

    例 10.11: 测试指定方法,该方法被调用一次,并检查调用时的参数

    <?php
    class SubjectTest extends PHPUnit_Framework_TestCase {
    
        public function testObserversAreUpdated() {
            // Create a mock for the Observer class, 
            // only mock the update() method. 
            $observer = $this->getMock('Observer', array('update'));
            
            // Set up the expectation for the update() method 
            // to be called only once and with the string 'something' 
            // as its parameter. 
            $observer->expects($this->once())
                     ->method('update')
                     ->with($this->equalTo('something'));
            
            // Create a Subject object and attach the mocked 
            // Observer object to it. 
            $subject = new Subject();
            $subject->attach($observer);
            
            // Call the doSomething() method on the $subject object 
            // which we expect to call the mocked Observer object's 
            // update() method with the string 'something'. 
            $subject->doSomething();
        }
    }
    ?> 

    with()方法可以有任意个参数,对应与被mocked的方法的参数个数,你可以对调用参数使用更加高级的的约束,如:

    例 10.12: 测试指定方法,并使用不同的方式对该方法调用时的参数进行约束

    <?php
    class SubjectTest extends PHPUnit_Framework_TestCase {
    
        public function testErrorReported() {
            // Create a mock for the Observer class, mocking the 
            // reportError() method 
            $observer = $this->getMock('Observer', array('reportError'));
            
            $observer->expects($this->once())
                     ->method('reportError')
                     ->with($this->greaterThan(0),
                             $this->stringContains('Something'),
                             $this->anything());
            
            $subject = new Subject();
            $subject->attach($observer);
            
            // The doSomethingBad() method should report an error to the observer 
            // via the reportError() method 
            $subject->doSomethingBad();
        }
    }
    ?> 

    4.3中所示的方法可以用来约束被mock方法的参数,表10.1中的匹配可以用来指定方法被调用的次数:

    表10.1。 Machers

    匹配含义
    PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() 返回一个匹配相匹配时,它评估的方法被执行零次或多次。
    PHPUnit_Framework_MockObject_Matcher_InvokedCount never() 返回一个匹配,匹配的方法对其进行评估时,将不会被执行。
    PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce() 返回一个匹配,匹配的方法对其进行评估时,至少执行一次。
    PHPUnit_Framework_MockObject_Matcher_InvokedCount once() 返回一个匹配,匹配的方法对其进行评估时,被执行一次。
    PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count) 返回一个匹配,匹配的方法对其进行评估时,正确地执行了$count时间。
    PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) 返回一个匹配,在给定的$index匹配时调用的方法对其进行评估。注意:mock对象的任意方法被调用时,index都会加1。

    getMockForAbstractClass()方法可以为抽象类返回一个mock对象,所有的抽象方法都会被mock,而非抽象方法则不会被mock,这样我们就可以测试一个抽象类的非抽象方法了。如例10.13所示:

    例 10.13: 测试抽象类的非抽象方法

    <?php 
     abstract   class   AbstractClass 
     { 
          public   function   concreteMethod ( ) 
          { 
              return   $this -> abstractMethod ( ) ; 
          } 
     
          public   abstract   function   abstractMethod ( ) ; 
     } 
     
     class   AbstractClassTest   extends   PHPUnit_Framework_TestCase 
     { 
          public   function   testConcreteMethod ( ) 
          { 
              $stub   =   $this -> getMockForAbstractClass ( 'AbstractClass' ) ; 
              $stub -> expects ( $this -> any ( ) ) 
                   -> method ( 'abstractMethod' ) 
                   -> will ( $this -> returnValue ( TRUE ) ) ; 
     
              $this -> assertTrue ( $stub -> concreteMethod ( ) ) ; 
          } 
     } 
     ?>

    例 10.14: 测试一个方法,其获取到的参数与调用时的参数一致

    <?php
    class FooTest extends PHPUnit_Framework_TestCase {
    
        public function testIdenticalObjectPassed() {
            $expectedObject = new stdClass();
            
            $mock = $this->getMock('stdClass', array('foo'));
            $mock->expects($this->once())
                 ->method('foo')
                 ->with($this->identicalTo($expectedObject));
            
            $mock->foo($expectedObject);
        }
    }
    ?> 

    例 10.15: 当允许拷贝参数功能开启时,创建Mock对象

    <?php
    class FooTest extends PHPUnit_Framework_TestCase {
    
        public function testIdenticalObjectPassed() {
            $cloneArguments = true;
            
            $mock = $this->getMock('stdClass',
                                    array(),
                                    array(),
                                    '',
                                    FALSE,
                                    TRUE,
                                    TRUE,
                                    $cloneArguments);
            
            // or using the mock builder 
            $mock = $this->getMockBuilder('stdClass')
                         ->enableArgumentCloning()
                         ->getMock();
        
            // now your mock clones parameters so the identicalTo constraint will fail. 
        }
    }
    ?> 
  • 相关阅读:
    如何将u盘、移动硬盘转化为活动分区--绝招
    jstl错误排除:According to TLD or attribute directive in tag file, attribute value does not accept any expressions
    eclipse中package explore和project explore 怎么相互切换???
    硬盘知识区
    Sublime Text 3下Emmet使用技巧
    sublime text3中设置Emmet输入标签自动闭合
    window如何分区
    HTTP缓存
    react-router 4实现代码分割(code spliting)
    Vue练手项目(包含typescript版本)
  • 原文地址:https://www.cnblogs.com/jeJee/p/2747736.html
Copyright © 2020-2023  润新知