PHPUnit9.0 测试替身-Stubs(桩件)
将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为打桩(stubbing)。可以用桩件(Stub)来“替换掉被测系统所依赖的实际组件,这样测试就有了对被测系统的间接输入的控制点。这使得测试能强制安排被测系统的执行路径,否则被测系统可能无法执行”。
示例 8.2 展示了如何对方法的调用进行上桩以及如何设定返回值。首先用 PHPUnit\Framework\TestCase
类提供的 createStub()
方法来建立一个桩件对象,它表面看起来像是 SomeClass
类(示例 8.1)的实例。随后用 PHPUnit 提供的流畅式接口来指定桩件的行为。本质上,这意味着不需要建立多个临时对象然后再把它们捆到一起。取而代之的是范例中所示的链式方法调用。这使得代码更加易读并更加“流畅”。
示例 8.1 想要上桩的类
<?php declare(strict_types=1);
class SomeClass
{
public function doSomething()
{
// 随便做点什么。
}
}
示例 8.2 对某个方法的调用进行上桩,返回固定值
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testStub(): void
{
// 为 SomeClass 类创建桩件。
$stub = $this->createStub(SomeClass::class);
// 配置桩件。
$stub->method('doSomething')
->willReturn('foo');
// 现在调用 $stub->doSomething() 会返回 'foo'。
$this->assertSame('foo', $stub->doSomething());
}
仅当原始类中不包含名字为“method”的方法时,以上范例才能正常运行。
如果原始类包含名为“method”的方法,就必须用 $stub->expects($this->any())->method('doSomething')->willReturn('foo');
“在幕后”,当使用了 createStub()
方法时, PHPUnit 自动生成了一个新的 PHP 类来实现想要的行为。
请注意:createStub()
会自动递归地基于方法的返回类型对返回值进行上桩。考虑以下示例:
示例 8.3 带有返回类型声明的方法
<?php declare(strict_types=1);
class C
{
public function m(): D
{
// 随便做点什么。
}
}
在上述示例中,C::m()
方法具有返回类型声明,指示此方法返回类型为 D
的对象。那么,举个例子说,创建 C
的测试替身而又未用 willReturn()
给 m()
配置返回值时,则当 PHPUnit 调用 m()
时会自动创建一个 D
的测试替身作为返回值。
类似地,如果 m
的返回类型声明是标量类型,则会生成诸如 0
(对于 int
)、0.0
(对于 float
)、或 []
(对于 array
)这样的返回值。
示例 8.4 展示了如何用仿件生成器的流畅式接口来配置测试替身的生成。这个测试替身的默认配置用的是和 createStub()
相同的最佳实践。
示例 8.4 使用可用于配置生成的测试替身类的仿件生成器 API
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testStub(): void
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder(SomeClass::class)
->disableOriginalConstructor()
->disableOriginalClone()
->disableArgumentCloning()
->disallowMockingUnknownTypes()
->getMock();
// 配置桩件。
$stub->method('doSomething')
->willReturn('foo');
// 现在调用 $stub->doSomething() 会返回 'foo'。
$this->assertSame('foo', $stub->doSomething());
}
在之前的例子中,用 willReturn($value)
返回简单值。这个简短的语法相当于 will($this->returnValue($value))
。而在这个长点的语法中,可以使用变量,从而实现更复杂的上桩行为。
有时想要将(未改变的)方法调用时所使用的参数之一作为桩件的方法的调用结果来返回。示例 8.5 展示了如何用 returnArgument()
代替 returnValue()
来做到这点。
示例 8.5 对某个方法的调用进行上桩,返回参数之一
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testReturnArgumentStub(): void
{
// 为 SomeClass 类创建桩件。
$stub = $this->createStub(SomeClass::class);
// 配置桩件。
$stub->method('doSomething')
->will($this->returnArgument(0));
// $stub->doSomething('foo') 返回 'foo'
$this->assertSame('foo', $stub->doSomething('foo'));
// $stub->doSomething('bar') 返回 'bar'
$this->assertSame('bar', $stub->doSomething('bar'));
}
}
在用流畅式接口进行测试时,让某个已上桩的方法返回对桩件对象的引用有时会很有用。示例 8.6 展示了如何用 returnSelf()
来做到这点。
示例 8.6 对方法的调用进行上桩,返回对桩件对象的引用
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testReturnSelf(): void
{
// 为 SomeClass 类创建桩件。
$stub = $this->createStub(SomeClass::class);
// 配置桩件。
$stub->method('doSomething')
->will($this->returnSelf());
// $stub->doSomething() 返回 $stub
$this->assertSame($stub, $stub->doSomething());
}
}
有时候,上桩的方法需要根据预定义的参数清单来返回不同的值。可以用 returnValueMap()
方法将参数和相应的返回值关联起来建立映射。示例参见示例 8.7。
示例 8.7 对方法的调用进行上桩,按照映射确定返回值
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testReturnValueMapStub(): void
{
// 为 SomeClass 类创建桩件。
$stub = $this->createStub(SomeClass::class);
// Create a map of arguments to return values.
$map = [
['a', 'b', 'c', 'd'],
['e', 'f', 'g', 'h']
];
// 配置桩件。
$stub->method('doSomething')
->will($this->returnValueMap($map));
// $stub->doSomething() 根据提供的参数返回不同的值。
$this->assertSame('d', $stub->doSomething('a', 'b', 'c'));
$this->assertSame('h', $stub->doSomething('e', 'f', 'g'));
}
}
如果上桩的方法需要返回计算得到的值而不是固定值(参见 returnValue()
)或某个(未改变的)参数(参见 returnArgument()
),可以用 returnCallback()
来让上桩的方法返回回调函数或方法的结果。示例参见示例 8.8。
示例 8.8 对方法的调用进行上桩,由回调生成返回值
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testReturnCallbackStub(): void
{
// 为 SomeClass 类创建桩件。
$stub = $this->createStub(SomeClass::class);
// 配置桩件。
$stub->method('doSomething')
->will($this->returnCallback('str_rot13'));
// $stub->doSomething($argument) 返回 str_rot13($argument)
$this->assertSame('fbzrguvat', $stub->doSomething('something'));
}
}
相比于建立回调方法,有一个更简单的选择是直接给出期望返回值的列表。可以用 onConsecutiveCalls()
方法来做到这个。示例参见示例 8.9。
示例 8.9 对方法的调用上桩,按照指定顺序返回列表中的值
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testOnConsecutiveCallsStub(): void
{
// 为 SomeClass 类创建桩件。
$stub = $this->createStub(SomeClass::class);
// 配置桩件。
$stub->method('doSomething')
->will($this->onConsecutiveCalls(2, 3, 5, 7));
// $stub->doSomething() 每次都会返回不同的值
$this->assertSame(2, $stub->doSomething());
$this->assertSame(3, $stub->doSomething());
$this->assertSame(5, $stub->doSomething());
}
}
除了返回一个值之外,上桩的方法还能抛出一个异常。示例 8.10 展示了如何用 throwException()
做到这点。
示例 8.10 对方法的调用进行上桩,抛出异常
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubTest extends TestCase
{
public function testThrowExceptionStub(): void
{
// 为 SomeClass 类创建桩件。
$stub = $this->createStub(SomeClass::class);
// 配置桩件。
$stub->method('doSomething')
->will($this->throwException(new Exception));
// $stub->doSomething() 抛出异常
$stub->doSomething();
}
}
另外,也可以自行编写桩件,并在此过程中改善设计。在系统中被广泛使用的资源是通过单个外观(facade)来访问的,因此就能用桩件替换掉资源。例如,将散落在代码各处的对数据库的直接调用替换为单个 Database
对象,这个对象实现了 IDatabase
接口。接下来,就可以创建实现了 IDatabase
的桩件并在测试中使用之。甚至可以创建一个选项来控制是用桩件还是用真实数据库来运行测试,这样测试就既能在开发过程中用作本地测试,又能在实际数据库环境中进行集成测试。
需要上桩的功能往往集中在同一个对象中,这就改善了内聚度。将功能通过单一且一致的接口呈现出来,就降低了这部分与系统其他部分之间的耦合度。
更多建议: