Stubs (桩件)
将对象替换为(可选地)返回配置好的返回值的测试替身的实践方法称为上桩(stubbing)。可以用桩件(stub)来“替换掉被测系统所依赖的实际组件,这样测试就有了对被测系统的间接输入的控制点。这使得测试能强制安排被测系统的执行路径,否则被测系统可能无法执行”。
Example 9.2, “对某个方法的调用上桩,返回固定值”展示了如何对方法的调用上桩以及如何设定返回值。首先用 PHPUnit_Framework_TestCase
类提供的 getMockBuilder()
方法来建立一个桩件对象,它表面看起来像是 SomeClass
类(Example 9.1, “需要对其上桩的类”)的实例。随后用 PHPUnit 提供的 流畅式接口来指定桩件的行为。本质上,这意味着不需要建立多个临时对象然后再把它们捆到一起。取而代之的是范例中所示的链式方法调用。这使得代码更加易读并更加“流畅”。
Example 9.1. 需要对其上桩的类
Example 9.2. 对某个方法的调用上桩,返回固定值
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
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, “对某个方法的调用上桩,返回参数之一”展示了如何用 returnArgument()
代替 returnValue()
来做到这点。
Example 9.4. 对某个方法的调用上桩,返回参数之一
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, “对方法的调用上桩,返回对桩件对象的引用”展示了如何用 returnSelf()
来做到这点。
Example 9.5. 对方法的调用上桩,返回对桩件对象的引用
getMockBuilder('SomeClass')
->getMock();
// 配置桩件。
$stub->method('doSomething')
->will($this->returnSelf());
// $stub->doSomething() 返回 $stub
$this->assertSame($stub, $stub->doSomething());
}
}
?>
有时候,上桩的方法需要根据预定义的参数清单来返回不同的值。可以用 returnValueMap()
方法将参数和相应的返回值关联起来建立映射。范例参见Example 9.6, “对方法的调用上桩,按照映射确定返回值”。
Example 9.6. 对方法的调用上桩,按照映射确定返回值
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. 对方法的调用上桩,由回调生成返回值
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. 对方法的调用上桩,按照指定顺序返回列表中的值
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, “对方法的调用上桩,抛出异常”展示了如何用 throwException()
做到这点。
Example 9.9. 对方法的调用上桩,抛出异常
getMockBuilder('SomeClass')
->getMock();
// 配置桩件。
$stub->method('doSomething')
->will($this->throwException(new Exception));
// $stub->doSomething() 抛出异常
$stub->doSomething();
}
}
?>
另外,也可以自行编写桩件,并在此过程中改善设计。在系统中被广泛使用的资源是通过单个外观(facade)来访问的,因此很容易就能用桩件替换掉资源。例如,将散落在代码各处的对数据库的直接调用替换为单个 Database
对象,这个对象实现了 IDatabase
接口。接下来,就可以创建实现了 IDatabase
的桩件并在测试中使用之。甚至可以创建一个选项来控制是用桩件还是用真实数据库来运行测试,这样测试就既能在开发过程中用作本地测试,又能在实际数据库环境中进行集成测试。
需要上桩的功能往往集中在同一个对象中,这就改善了内聚度。将功能通过单一且一致的界面呈现出来,就降低了这部分与系统其他部分之间的耦合度。