# Stubs (桩件)
将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为*上桩(stubbing)*。可以用*桩件(stub)*来“替换掉被测系统所依赖的实际组件,这样测试就有了对被测系统的间接输入的控制点。这使得测试能强制安排被测系统的执行路径,否则被测系统可能无法执行”。
[Example 9.2, “对某个方法的调用上桩,返回固定值”](# "Example 9.2. 对某个方法的调用上桩,返回固定值")展示了如何对方法的调用上桩以及如何设定返回值。首先用 `PHPUnit_Framework_TestCase` 类提供的 `getMockBuilder()` 方法来建立一个桩件对象,它表面看起来像是 `SomeClass`类([Example 9.1, “需要对其上桩的类”](# "Example 9.1. 需要对其上桩的类"))的实例。随后用 PHPUnit 提供的 [流畅式接口](http://martinfowler.com/bliki/FluentInterface.html)来指定桩件的行为。本质上,这意味着不需要建立多个临时对象然后再把它们捆到一起。取而代之的是范例中所示的链式方法调用。这使得代码更加易读并更加“流畅”。
**Example 9.1. 需要对其上桩的类**
~~~
<?php
class SomeClass
{
public function doSomething()
{
// 随便做点什么。
}
}
?>
~~~
**Example 9.2. 对某个方法的调用上桩,返回固定值**
~~~
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder('SomeClass')
->getMock();
// 配置桩件。
$stub->method('doSomething')
->willReturn('foo');
// 现在调用 $stub->doSomething() 将返回 'foo'。
$this->assertEquals('foo', $stub->doSomething());
}
}
?>
~~~
### 局限性:名字为“method”的方法
仅当原始类中不包含名字为“method”的方法时,以上范例才能正常运行。
如果原始类包含名为“method”的方法,就必须用
~~~
$stub->expects($this->any())->method('doSomething')->willReturn('foo');
~~~
“在幕后”,当使用了 `getMock()` 方法时, PHPUnit 自动生成了一个新的 PHP 类来实现想要的行为。
[Example 9.3, “使用可用于配置生成的测试替身类的仿件生成器 API”](# "Example 9.3. 使用可用于配置生成的测试替身类的仿件生成器 API")这个例子展示了如何用仿件生成器的流畅式接口来配置测试替身的生成。
**Example 9.3. 使用可用于配置生成的测试替身类的仿件生成器 API**
~~~
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder('SomeClass')
->disableOriginalConstructor()
->getMock();
// 配置桩件。
$stub->method('doSomething')
->willReturn('foo');
// 现在调用 $stub->doSomething() 将返回 'foo'。
$this->assertEquals('foo', $stub->doSomething());
}
}
?>
~~~
以下是仿件生成器提供的方法列表:
-
`setMethods(array $methods)` 可以在仿件生成器对象上调用,来指定哪些方法将被替换为可配置的测试替身。其他方法的行为不会有所改变。如果调用 `setMethods(null)`,那么没有方法会被替换。
-
`setConstructorArgs(array $args)` 可用于向原版类的构造函数(默认情况下不会被替换为伪实现)提供参数数组。
-
`setMockClassName($name)` 可用于指定生成的测试替身类的类名。
-
`disableOriginalConstructor()` 参数可用于禁用对原版类的构造方法的调用。
-
`disableOriginalClone()` 可用于禁用对原版类的克隆方法的调用。
-
`disableAutoload()`可用于在测试替身类的生成期间禁用 `__autoload()`。
在之前的例子中,用 `willReturn($value)` 返回简单值。这个简短的语法相当于 `will($this->returnValue($value))`。而在这个长点的语法中,可以使用变量,从而实现更复杂的上桩行为。
有时想要将(未改变的)方法调用时所使用的参数之一作为桩件的方法的调用结果来返回。 [Example 9.4, “对某个方法的调用上桩,返回参数之一”](# "Example 9.4. 对某个方法的调用上桩,返回参数之一")展示了如何用 `returnArgument()` 代替 `returnValue()` 来做到这点。
**Example 9.4. 对某个方法的调用上桩,返回参数之一**
~~~
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnArgumentStub()
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder('SomeClass')
->getMock();
// 配置桩件。
$stub->method('doSomething')
->will($this->returnArgument(0));
// stub->doSomething('foo') 返回 'foo'
$this->assertEquals('foo', $stub->doSomething('foo'));
// $stub->doSomething('bar') 返回 'bar'
$this->assertEquals('bar', $stub->doSomething('bar'));
}
}
?>
~~~
在用流畅式接口进行测试时,让某个已上桩的方法返回对桩件对象的引用有时会很有用。[Example 9.5, “对方法的调用上桩,返回对桩件对象的引用”](# "Example 9.5. 对方法的调用上桩,返回对桩件对象的引用")展示了如何用 `returnSelf()` 来做到这点。
**Example 9.5. 对方法的调用上桩,返回对桩件对象的引用**
~~~
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnSelf()
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder('SomeClass')
->getMock();
// 配置桩件。
$stub->method('doSomething')
->will($this->returnSelf());
// $stub->doSomething() 返回 $stub
$this->assertSame($stub, $stub->doSomething());
}
}
?>
~~~
有时候,上桩的方法需要根据预定义的参数清单来返回不同的值。可以用 `returnValueMap()` 方法将参数和相应的返回值关联起来建立映射。范例参见[Example 9.6, “对方法的调用上桩,按照映射确定返回值”](# "Example 9.6. 对方法的调用上桩,按照映射确定返回值")。
**Example 9.6. 对方法的调用上桩,按照映射确定返回值**
~~~
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnValueMapStub()
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder('SomeClass')
->getMock();
// 创建从参数到返回值的映射。
$map = array(
array('a', 'b', 'c', 'd'),
array('e', 'f', 'g', 'h')
);
// 配置桩件。
$stub->method('doSomething')
->will($this->returnValueMap($map));
// $stub->doSomething() 根据提供的参数返回不同的值。
$this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
$this->assertEquals('h', $stub->doSomething('e', 'f', 'g'));
}
}
?>
~~~
如果上桩的方法需要返回计算得到的值而不是固定值(参见 `returnValue()`)或某个(未改变的)参数(参见 `returnArgument()`),可以用 `returnCallback()` 来让上桩的方法返回回调函数或方法的结果。范例参见[Example 9.7, “对方法的调用上桩,由回调生成返回值”](# "Example 9.7. 对方法的调用上桩,由回调生成返回值")。
**Example 9.7. 对方法的调用上桩,由回调生成返回值**
~~~
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnCallbackStub()
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder('SomeClass')
->getMock();
// 配置桩件。
$stub->method('doSomething')
->will($this->returnCallback('str_rot13'));
// $stub->doSomething($argument) 返回 str_rot13($argument)
$this->assertEquals('fbzrguvat', $stub->doSomething('something'));
}
}
?>
~~~
相比于建立回调方法,有一个更简单的选择是直接给出期望返回值的列表。可以用 `onConsecutiveCalls()` 方法来做到这个。范例参见 [Example 9.8, “对方法的调用上桩,按照指定顺序返回列表中的值”](# "Example 9.8. 对方法的调用上桩,按照指定顺序返回列表中的值")。
**Example 9.8. 对方法的调用上桩,按照指定顺序返回列表中的值**
~~~
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testOnConsecutiveCallsStub()
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder('SomeClass')
->getMock();
// 配置桩件。
$stub->method('doSomething')
->will($this->onConsecutiveCalls(2, 3, 5, 7));
// $stub->doSomething() 每次返回值都不同
$this->assertEquals(2, $stub->doSomething());
$this->assertEquals(3, $stub->doSomething());
$this->assertEquals(5, $stub->doSomething());
}
}
?>
~~~
除了返回一个值之外,上桩的方法还能抛出一个异常。[Example 9.9, “对方法的调用上桩,抛出异常”](# "Example 9.9. 对方法的调用上桩,抛出异常")展示了如何用 `throwException()` 做到这点。
**Example 9.9. 对方法的调用上桩,抛出异常**
~~~
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testThrowExceptionStub()
{
// 为 SomeClass 类创建桩件。
$stub = $this->getMockBuilder('SomeClass')
->getMock();
// 配置桩件。
$stub->method('doSomething')
->will($this->throwException(new Exception));
// $stub->doSomething() 抛出异常
$stub->doSomething();
}
}
?>
~~~
另外,也可以自行编写桩件,并在此过程中改善设计。在系统中被广泛使用的资源是通过单个外观(facade)来访问的,因此很容易就能用桩件替换掉资源。例如,将散落在代码各处的对数据库的直接调用替换为单个 `Database` 对象,这个对象实现了 `IDatabase` 接口。接下来,就可以创建实现了 `IDatabase` 的桩件并在测试中使用之。甚至可以创建一个选项来控制是用桩件还是用真实数据库来运行测试,这样测试就既能在开发过程中用作本地测试,又能在实际数据库环境中进行集成测试。
需要上桩的功能往往集中在同一个对象中,这就改善了内聚度。将功能通过单一且一致的界面呈现出来,就降低了这部分与系统其他部分之间的耦合度。
- PHPUnit 手册
- 1. 安装 PHPUnit
- 需求
- PHP 档案包 (PHAR)
- Composer
- 可选的组件包
- 2. 编写 PHPUnit 测试
- 测试的依赖关系
- 数据供给器
- 对异常进行测试
- 对 PHP 错误进行测试
- 对输出进行测试
- 错误相关信息的输出
- 3. 命令行测试执行器
- 命令行选项
- 4. 基境(fixture)
- setUp() 多 tearDown() 少
- 变体
- 基境共享
- 全局状态
- 5. 组织测试
- 用文件系统来编排测试套件
- 用 XML 配置来编排测试套件
- 6. 有风险的测试
- 无用测试
- 意外的代码覆盖
- 测试执行期间产生的输出
- 测试执行时长的超时限制
- 全局状态篡改
- 7. 未完成的测试与跳过的测试
- 未完成的测试
- 跳过测试
- 用 @requires 来跳过测试
- 8. 数据库测试
- 数据库测试所支持的供应商
- 数据库测试的难点
- 数据库测试的四个阶段
- PHPUnit 数据库测试用例的配置
- 理解 DataSet(数据集)和 DataTable(数据表)
- 数据库连接 API
- 数据库断言 API
- 常见问题(FAQ)
- 9. 测试替身
- Stubs (桩件)
- 仿件对象(Mock Object)
- Prophecy
- 对特质(Trait)与抽象类进行模仿
- 对 Web 服务(Web Services)进行上桩或模仿
- 对文件系统进行模仿
- 10. 测试实践
- 在开发过程中
- 在调试过程中
- 11. 代码覆盖率分析
- 用于代码覆盖率的软件衡量标准
- 包含与排除文件
- 略过代码块
- 指明要覆盖的方法
- 边缘情况
- 12. 测试的其他用途
- 敏捷文档
- 跨团队测试
- 13. Logging (日志记录)
- 测试结果 (XML)
- 测试结果 (TAP)
- 测试结果 (JSON)
- 代码覆盖率 (XML)
- 代码覆盖率 (TEXT)
- 14. 扩展 PHPUnit
- 从 PHPUnit_Framework_TestCase 派生子类
- 编写自定义断言
- 实现 PHPUnit_Framework_TestListener
- 从 PHPUnit_Extensions_TestDecorator 派生子类
- 实现 PHPUnit_Framework_Test
- A. 断言
- assertArrayHasKey()
- assertClassHasAttribute()
- assertArraySubset()
- assertClassHasStaticAttribute()
- assertContains()
- assertContainsOnly()
- assertContainsOnlyInstancesOf()
- assertCount()
- assertEmpty()
- assertEqualXMLStructure()
- assertEquals()
- assertFalse()
- assertFileEquals()
- assertFileExists()
- assertGreaterThan()
- assertGreaterThanOrEqual()
- assertInfinite()
- assertInstanceOf()
- assertInternalType()
- assertJsonFileEqualsJsonFile()
- assertJsonStringEqualsJsonFile()
- assertJsonStringEqualsJsonString()
- assertLessThan()
- assertLessThanOrEqual()
- assertNan()
- assertNull()
- assertObjectHasAttribute()
- assertRegExp()
- assertStringMatchesFormat()
- assertStringMatchesFormatFile()
- assertSame()
- assertStringEndsWith()
- assertStringEqualsFile()
- assertStringStartsWith()
- assertThat()
- assertTrue()
- assertXmlFileEqualsXmlFile()
- assertXmlStringEqualsXmlFile()
- assertXmlStringEqualsXmlString()
- B. 标注
- @author
- @after
- @afterClass
- @backupGlobals
- @backupStaticAttributes
- @before
- @beforeClass
- @codeCoverageIgnore*
- @covers
- @coversDefaultClass
- @coversNothing
- @dataProvider
- @depends
- @expectedException
- @expectedExceptionCode
- @expectedExceptionMessage
- @expectedExceptionMessageRegExp
- @group
- @large
- @medium
- @preserveGlobalState
- @requires
- @runTestsInSeparateProcesses
- @runInSeparateProcess
- @small
- @test
- @testdox
- @ticket
- @uses
- C. XML 配置文件
- PHPUnit
- 测试套件
- 分组
- 为代码覆盖率包含或排除文件
- Logging (日志记录)
- 测试监听器
- 设定 PHP INI 设置、常量、全局变量
- 为 Selenium RC 配置浏览器
- D. 升级
- E. 索引
- F. 参考书目
- G. 版权