php插桩,PHP | PHPUnit单元测试利器:PHP Mock的使用方法

5f21b83631127c88fef2639d476b9cf3.png

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

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

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

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

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

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

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

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

例 10.1: 待插桩的类

class SomeClass {

public function doSomething() {

// Do something.

}

}

?>

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

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来配置生成的测试替身类

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: 对指定方法打桩,并让其返回指定传入参数

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: 对指定方法打桩并使其返回打桩对象本身

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里配置的值

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: 对指定方法打桩,并使其返回值为指定函数调用的返回值

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: 对指定方法打桩,并使其返回值按指定序列逐次返回

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 都是测试系统的一部分

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对象来测试Subject和Observer对象之间的相互作用:

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

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: 测试指定方法,并使用不同的方式对该方法调用时的参数进行约束

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: 测试抽象类的非抽象方法

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: 测试一个方法,其获取到的参数与调用时的参数一致

53a22a72580a82cebd737520d755801b.gif

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对象

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.

}

}

?>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值