PHPUnit 提供的
createMock($type)
和getMockBuilder($type)
方法可以在测试中用来自动生成对象,此对象可以充当任意指定原版类型(接口或类名)的测试替身。在任何预期或要求使用原版类的实例对象的上下文中都可以使用这个测试替身对象来代替。
createMock($type)
方法直接返回指定类型(接口或类)的测试替身对象实例。此测试替身的创建使用了最佳实践的默认值(不执行原始类的__construct()
和__clone()
方法,且不对传递给测试替身的方法的参数进行克隆)。如果这些默认值非你所需,可以用getMockBuilder($type)
方法并使用流畅式接口来定制测试替身的生成过程。在默认情况下,原版类的所有方法都会被替换为只会返回
null
的伪实现(其中不会调用原版方法)。使用诸如will($this->returnValue())
之类的方法可以对这些伪实现在被调用时应当返回什么值做出配置。
简单来说就是一些测试类需要调用其他的类作为参数,但是我们又不想创建那些类,所以PHPUnit提供了一种模仿机制。
将对象替换成配置好对应返回值的替身的方法成为上桩(stubbing)。
可以返回的数据种类有:
下面是demo
require_once __DIR__."/../../vendor/autoload.php";
use PHPUnit\Framework\TestCase;
class SomeClass
{
public function doSomething()
{
// 随便做点什么。
return "data";
}
public function callUndefinedFunction($param){
return "你不可以调用在原始类中未定义的方法";
}
public function classTest(){
return "self";
}
public function returnValueByParam(){
}
public function returnDataByCallBack(){
}
public function returnDataByList(){
}
public function throwExceptions(){
}
}
class Mock extends TestCase{
public function testExample(){
$stub = $this->createMock(SomeClass::class);
// 返回固定值
$stub->method('doSomething')
->willReturn('foo');
// 返回参数
$stub->method("callUndefinedFunction")
->will($this->returnArgument(0));
// 返回该桩类自身
$stub->method("classTest")
->will($this->returnSelf());
// 根据不同参数,返回不同的值
$map=array(
// 最后一个值永远是返回的结果,而前面的就是传递的参数
array("param1","param2","param3",'result1'),
array('param2','param3','result2'),
array("result3")
);
$stub->method("returnValueByParam")
->will($this->returnValueMap($map));
// 下面调用时传入多少个参数,都会自动传入这里。
// 但是,下面调用的参数只能比这里多,不能少,否则会报错误
$callBackFunction=function ($param1,$param2,$param3){
print_r(array(
$param1,
$param2,
$param3
));
};
$stub->method("returnDataByCallBack")
->will($this->returnCallback($callBackFunction));
// 设置每一次调用的返回值
$stub->method("returnDataByList")
->will($this->onConsecutiveCalls(1,2,3,4,5,6));// 注意,这里传入的不是数组!
// 设置代码抛出异常
$stub->method("throwExceptions")
->will($this->throwException(new \Exception("没事情,就是抛异常玩玩")));
$this->assertEquals('foo', $stub->doSomething());
$this->assertEquals("hello",$stub->callUndefinedFunction("hello"));
$this->assertSame($stub,$stub->classTest());
$this->assertEquals("result1",$stub->returnValueByParam("param1","param2","param3"));
$this->assertEquals("result2",$stub->returnValueByParam('param2','param3'));
$this->assertEquals("result3",$stub->returnValueByParam());
// 这里调用上面设置的方法,其中的参数会自动传入回调函数中
// $stub->returnDataByCallBack(1,2,3,4);
$this->assertEquals(1,$stub->returnDataByList());
$this->assertEquals(2,$stub->returnDataByList());
try{
$stub->throwExceptions();
}catch (Exception $e){
print "\r\n".$e->getMessage()."\r\n";
}
}
}
注意,不能使用$stub->method()
设置对应类中不存在的方法,否则会报错。
public function testStubSetting(){
$stub=$this->getMockBuilder(SomeClassSecond::class)
->disableOriginalClone()// 开启设置之后,原类的 __clone 函数就不会运行了
->disableOriginalConstructor()// 开启这个之后,就不会运行原类的 __construct 方法了
->disableArgumentCloning()// 开启这个时候就不会在创建桩件的时候,复制原类的属性
->disallowMockingUnknownTypes()// 不允许定义不存在的属性
->getMock();
$stub2= clone $stub;// 即使上面限制了 disableOriginalClone,但是那只是限制不能运行原类的 __clone,clone函数还是生效的,所以下面的断言是正确的
$this->assertEquals($stub2,$stub,"两个类不一致");
}
仿件对象有桩件的功能,但是比桩件更加贴合原来的类。举个例子就是桩件只能调用设置好的方法,不能调用原类的一些方法,但是仿件对象可以。
require_once __DIR__."/../../vendor/autoload.php";
use PHPUnit\Framework\TestCase;
class Subject
{
protected $observers = [];
protected $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function attach(Observer $observer)
{
$this->observers[] = $observer;
}
public function doSomething()
{
// 做点什么
// ...
// 通知观察者发生了些什么
$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);
}
}
// 其他方法。
}
class Observer
{
public function update($argument)
{
// 做点什么。
print "\r\n\{$argument}r\n";
}
public function reportError($errorCode, $errorMessage, Subject $subject)
{
// 做点什么。
}
public function selfFunction(){
print "\r\nSelfFunction\r\n";
}
// 其他方法。
}
class MockObjectTest extends TestCase{
public function testCopy(){
// 为 Observer 类建立仿件对象,只模仿 update() 方法。
$observer = $this->getMockBuilder(Observer::class)
->setMethods(['update'])
->getMock();
// 建立预期状况:update() 方法将会被调用一次。只能调用一次,不能多不能少
// 并且将以字符串 'something' 为参数,不能以其他字符串作为参数
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));// 设置参数一定是 something
$observer->selfFunction();// 这里只是尝试调用原类的方法
// 创建 Subject 对象,并将模仿的 Observer 对象连接其上。
$subject = new Subject('My subject');
$subject->attach($observer);
// 在 $subject 对象上调用 doSomething() 方法,
// 预期将以字符串 'something' 为参数调用
// Observer 仿件对象的 update() 方法。
$subject->doSomething();
}
}
注意这里的一些细节:
setMethods($methods=array())
$this->once()
$this->equalTo()
with()
方法可以携带任何数量的参数,对应于被模仿的方法的参数数量。可以对方法的参数指定更加高等的约束而不仅是简单的匹配。
public function testErrorReported()
{
// 为 Observer 类建立仿件,对 reportError() 方法进行模仿
$observer = $this->getMockBuilder(Observer::class)
->setMethods(['reportError'])
->getMock();
$observer->expects($this->once())
->method('reportError')
->with(
$this->greaterThan(0),
$this->stringContains('Something'),
$this->anything()
);
$subject = new Subject('My subject');
$subject->attach($observer);
// doSomethingBad() 方法应当会通过(observer的)reportError()方法
// 向 observer 报告错误。
$subject->doSomethingBad();
}
withConsecutive()
方法可以接受任意多个数组作为参数,具体数量取决于欲测试的调用。每个数组都都是对被仿方法的相应参数的一组约束,就像with()
中那样。
这个单词分开解释就是连续的with
,简单来说就是设置该方法多次调用时,设置每次的预期。
public function testFunctionCalledTwoTimesWithSpecificArguments()
{
$mock = $this->getMockBuilder(stdClass::class)
->setMethods(['set'])
->getMock();
$mock->expects($this->exactly(2))
->method('set')
// consecutive 英文翻译 连续的
->withConsecutive(
[$this->equalTo('foo'), $this->greaterThan(0)],
[$this->equalTo('bar'), $this->greaterThan(0)]
);
$mock->set('foo', 21);
$mock->set('bar', 48);
}
上面我们设置了一个方法只调用一次用了$this->once()
,如果我们要设置一个方法被调用N次,则需要使用$this->exactly()
方法。
这个语言很难解释,但是看代码就可以理解了:
public function testErrorReported()
{
// 为 Observer 类建立仿件,模仿 reportError() 方法
$observer = $this->getMockBuilder(Observer::class)
->setMethods(['reportError'])
->getMock();
$observer->expects($this->once())
->method('reportError')
->with($this->greaterThan(0),
$this->stringContains('Something'),
// 上面将对应原本本身传入
$this->callback(function($subject){
return is_callable([$subject, 'getName']) &&
$subject->getName() == 'My subject';
}));
$subject = new Subject('My subject');
$subject->attach($observer);
// doSomethingBad() 方法应当会通过(observer的)reportError()方法
// 向 observer 报告错误。
$subject->doSomethingBad();
}
public function testIdenticalObjectPassed()
{
$expectedObject = new stdClass;
$mock = $this->getMockBuilder(stdClass::class)
->setMethods(['foo'])
->getMock();
$mock->expects($this->once())
->method('foo')
// 预期参数将与 stdClass 一样
->with($this->identicalTo($expectedObject));
$mock->foo($expectedObject);
}
exactly(int $count)
at(int $index)
setMethods(array $methods)
:设置覆盖的方法setConstructorArgs(array $args)
:向原类的__constructor
方法传递参数disableOriginalConstructor
:禁用原类的__constructor
方法disableOriginalClone
:禁用原类的__clone
方法这玩意是另一个测试框架,有机会学习吧。(下辈子吧)
针对trait
来说,就是$this->getMockForTrait(AbstractTrait::class);
,剩下的就是一样的了。
getMockForAbstractClass()
方法返回一个抽象类的仿件对象。给定抽象类的所有抽象方法将都被模仿。这样就能对抽象类的具体方法进行测试。
require_once __DIR__."/../../vendor/autoload.php";
use PHPUnit\Framework\TestCase;
trait AbstractTrait
{
public function concreteMethod()
{
return $this->abstractMethod();
}
public abstract function abstractMethod();
}
class classTest extends TestCase{
public function testConcreteMethod()
{
$mock = $this->getMockForTrait(AbstractTrait::class);
$mock->expects($this->any())
->method('abstractMethod')
->will($this->returnValue(true));
$this->assertTrue($mock->concreteMethod());
}
}
针对abstract
来说也是一样:$this->getMockForAbstractClass(AbstractClass::class);
require_once __DIR__."/../../vendor/autoload.php";
use PHPUnit\Framework\TestCase;
abstract class AbstractClass
{
public function concreteMethod()
{
return $this->abstractMethod();
}
public abstract function abstractMethod();
}
class classTest extends TestCase{
public function testConcreteMethod()
{
$stub = $this->getMockForAbstractClass(AbstractClass::class);
$stub->expects($this->any())
->method('abstractMethod')
->will($this->returnValue(true));
$this->assertTrue($stub->concreteMethod());
}
}
拓展知识点太多了,以后有机会再说吧。
wsdl介绍
这里贴一下我找到的例子中的GoogleSearch.wsdl
的文件内容:
<definitions name="GoogleSearch"
targetNamespace="urn:GoogleSearch"
xmlns:typens="urn:GoogleSearch"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<types>
<xsd:schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:GoogleSearch">
<xsd:complexType name="GoogleSearchResult">
<xsd:all>
<xsd:element name="documentFiltering" type="xsd:boolean"/>
<xsd:element name="searchComments" type="xsd:string"/>
<xsd:element name="estimatedTotalResultsCount" type="xsd:int"/>
<xsd:element name="estimateIsExact" type="xsd:boolean"/>
<xsd:element name="resultElements" type="typens:ResultElementArray"/>
<xsd:element name="searchQuery" type="xsd:string"/>
<xsd:element name="startIndex" type="xsd:int"/>
<xsd:element name="endIndex" type="xsd:int"/>
<xsd:element name="searchTips" type="xsd:string"/>
<xsd:element name="directoryCategories" type="typens:DirectoryCategoryArray"/>
<xsd:element name="searchTime" type="xsd:double"/>
xsd:all>
xsd:complexType>
<xsd:complexType name="ResultElement">
<xsd:all>
<xsd:element name="summary" type="xsd:string"/>
<xsd:element name="URL" type="xsd:string"/>
<xsd:element name="snippet" type="xsd:string"/>
<xsd:element name="title" type="xsd:string"/>
<xsd:element name="cachedSize" type="xsd:string"/>
<xsd:element name="relatedInformationPresent" type="xsd:boolean"/>
<xsd:element name="hostName" type="xsd:string"/>
<xsd:element name="directoryCategory" type="typens:DirectoryCategory"/>
<xsd:element name="directoryTitle" type="xsd:string"/>
xsd:all>
xsd:complexType>
<xsd:complexType name="ResultElementArray">
<xsd:complexContent>
<xsd:restriction base="soapenc:Array">
<xsd:attribute ref="soapenc:arrayType" wsdl:arrayType="typens:ResultElement[]"/>
xsd:restriction>
xsd:complexContent>
xsd:complexType>
<xsd:complexType name="DirectoryCategoryArray">
<xsd:complexContent>
<xsd:restriction base="soapenc:Array">
<xsd:attribute ref="soapenc:arrayType" wsdl:arrayType="typens:DirectoryCategory[]"/>
xsd:restriction>
xsd:complexContent>
xsd:complexType>
<xsd:complexType name="DirectoryCategory">
<xsd:all>
<xsd:element name="fullViewableName" type="xsd:string"/>
<xsd:element name="specialEncoding" type="xsd:string"/>
xsd:all>
xsd:complexType>
xsd:schema>
types>
<message name="doGetCachedPage">
<part name="key" type="xsd:string"/>
<part name="url" type="xsd:string"/>
message>
<message name="doGetCachedPageResponse">
<part name="return" type="xsd:base64Binary"/>
message>
<message name="doSpellingSuggestion">
<part name="key" type="xsd:string"/>
<part name="phrase" type="xsd:string"/>
message>
<message name="doSpellingSuggestionResponse">
<part name="return" type="xsd:string"/>
message>
<message name="doGoogleSearch">
<part name="key" type="xsd:string"/>
<part name="q" type="xsd:string"/>
<part name="start" type="xsd:int"/>
<part name="maxResults" type="xsd:int"/>
<part name="filter" type="xsd:boolean"/>
<part name="restrict" type="xsd:string"/>
<part name="safeSearch" type="xsd:boolean"/>
<part name="lr" type="xsd:string"/>
<part name="ie" type="xsd:string"/>
<part name="oe" type="xsd:string"/>
message>
<message name="doGoogleSearchResponse">
<part name="return" type="typens:GoogleSearchResult"/>
message>
<portType name="GoogleSearchPort">
<operation name="doGetCachedPage">
<input message="typens:doGetCachedPage"/>
<output message="typens:doGetCachedPageResponse"/>
operation>
<operation name="doSpellingSuggestion">
<input message="typens:doSpellingSuggestion"/>
<output message="typens:doSpellingSuggestionResponse"/>
operation>
<operation name="doGoogleSearch">
<input message="typens:doGoogleSearch"/>
<output message="typens:doGoogleSearchResponse"/>
operation>
portType>
<binding name="GoogleSearchBinding" type="typens:GoogleSearchPort">
<soap:binding style="rpc"
transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="doGetCachedPage">
<soap:operation soapAction="urn:GoogleSearchAction"/>
<input>
<soap:body use="encoded"
namespace="urn:GoogleSearch"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
input>
<output>
<soap:body use="encoded"
namespace="urn:GoogleSearch"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
output>
operation>
<operation name="doSpellingSuggestion">
<soap:operation soapAction="urn:GoogleSearchAction"/>
<input>
<soap:body use="encoded"
namespace="urn:GoogleSearch"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
input>
<output>
<soap:body use="encoded"
namespace="urn:GoogleSearch"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
output>
operation>
<operation name="doGoogleSearch">
<soap:operation soapAction="urn:GoogleSearchAction"/>
<input>
<soap:body use="encoded"
namespace="urn:GoogleSearch"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
input>
<output>
<soap:body use="encoded"
namespace="urn:GoogleSearch"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
output>
operation>
binding>
<service name="GoogleSearchService">
<port name="GoogleSearchPort" binding="typens:GoogleSearchBinding">
<soap:address location="http://api.google.com/search/beta2"/>
port>
service>
definitions>
composer require mikey179/vfsStream
示例1主要演示的就是没有使用文件测试前,测试文件相关时的例子。
require_once __DIR__."/../../vendor/autoload.php";
use PHPUnit\Framework\TestCase;
class Example
{
protected $id;
protected $directory;
public function __construct($id)
{
$this->id = $id;
}
public function setDirectory($directory)
{
$this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;
if (!file_exists($this->directory)) {
mkdir($this->directory, 0700, true);
}
}
}
class ExampleTest extends TestCase
{
protected function setUp():void
{
if (file_exists(dirname(__FILE__) . '/id')) {
rmdir(dirname(__FILE__) . '/id');
}
}
public function testDirectoryIsCreated()
{
$example = new Example('id');
$this->assertFalse(file_exists(dirname(__FILE__) . '/id'));
$example->setDirectory(dirname(__FILE__));
$this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
}
protected function tearDown():void
{
if (file_exists(dirname(__FILE__) . '/id')) {
rmdir(dirname(__FILE__) . '/id');
}
}
}
这个麻烦就麻烦在文件创建完之后要写代码清除,如果清除不干净,则会对下次测试造成影响。比如在这里我们就使用了setUp
和tearDown
,来清除文件。
require_once __DIR__."/../../vendor/autoload.php";
use PHPUnit\Framework\TestCase;
class Example
{
protected $id;
protected $directory;
public function __construct($id)
{
$this->id = $id;
}
public function setDirectory($directory)
{
$this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;
if (!file_exists($this->directory)) {
mkdir($this->directory, 0700, true);
}
}
}
class ExampleTest extends TestCase
{
public function setUp():void
{
\org\bovigo\vfs\vfsStreamWrapper::register();
\org\bovigo\vfs\vfsStreamWrapper::setRoot(new \org\bovigo\vfs\vfsStreamDirectory('exampleDir'));
}
public function testDirectoryIsCreated()
{
$example = new Example('id');
$this->assertFalse(\org\bovigo\vfs\vfsStreamWrapper::getRoot()->hasChild('id'));
$example->setDirectory(\org\bovigo\vfs\vfsStream::url('exampleDir'));
$this->assertTrue(\org\bovigo\vfs\vfsStreamWrapper::getRoot()->hasChild('id'));
}
}
关于mikey179/vfsStream这个拓展类的更多介绍,这里我就不深究了,以后用到再说吧。