我正在尝试构建一个动态模拟(术语使用不严格),原因如下:
- 这个练习将帮助我了解更多关于测试的知识。
- 大多数嘲弄系统都满足不了我的需求。
- 大多数嘲弄系统都不能以我想要的方式做我需要的事情。
换句话说:请不要告诉我去使用一个mock库。我已经使用了mock库(我已经广泛使用了至少三个PHP库),我决定尝试创建自己的解决方案是有意识的。
所以:我正在尝试做一些概念上看起来相当简单的事情。
我如何动态地覆盖所有方法在我的模拟类存在于类的模拟扩展?
换句话说,如果我有包含方法a
的类A
,并且我有扩展类A
的模拟B
,我怎么能捕获对方法A
的所有调用而不显式地在模拟类B
中实现方法a
?
我试着用__call()
魔法方法来做这件事,但这不起作用,因为__call()
只捕获对不存在的方法的调用。
我想要避免需要大量架构更改的方法。我在这里的主要要求是,任何在其构造函数中需要类A
实例的类必须不能告诉mock B
不是类A
的实例。因此,我初步选择让模拟类B
扩展类A
。我也不希望对类A
进行大的更改,例如将其方法设置为private并让它使用__call()
。
像这样:
class A
{
public function foo()
{
return __CLASS__;
}
}
class B extends A
{
public function foo()
{
return __CLASS__;
}
}
$b = new B();
$reflection = new ReflectionObject($b);
$parentReflection = $reflection->getParentClass();
$parentFooReflection = $parentReflection->getMethod('foo');
$data = $parentFooReflection->invoke($b);
echo $data;
注意,在invoke(object $object, methods argument1, methods argument2...)
函数中,可以在第一个参数之后传递方法参数
你可以用Reflections做很多类似的事情,更多信息请查看链接http://php.net/manual/en/book.reflection.php
我接受了@dm03514的建议,跳进了Phokito的源代码。Phokito至少使用模拟构建器动态生成模拟类定义,该定义扩展基类并覆盖其所有方法,然后使用eval
声明已定义的类。
class MockFactory
{
public function buildMock( $class )
{
$reflection = new 'ReflectionClass( $class );
$mockedShortName = $reflection->getShortName();
$mockShortName = "Mock$mockedShortName";
$mockClass = "''$mockShortName";
if ( ! class_exists($mockClass) ) {
$this->declareMockClass( $reflection, $mockShortName, $mockedShortName );
}
return new $mockClass;
}
private function declareMockClass( $reflection, $mockShortName, $mockedShortName )
{
$php = [];
$mockedNamespace = $reflection->getNamespaceName();
$extends = $reflection->isInterface() ? 'implements' : 'extends';
$php[] = <<<EOT
class $mockShortName $extends $mockedNamespace''$mockedShortName {
public function setReturnValue( '$method, '$returnValue ) {
'$this->'$method = '$returnValue;
}
public function getCalls ( '$method ) {
'$callsProperty = '$method . "Calls";
return '$this->'$callsProperty;
}
EOT;
foreach ( $reflection->getMethods() as $method ) {
$methodName = $method->name;
$params = [];
foreach ( $method->getParameters() as $i => $parameter ) {
if ( $parameter->isArray() ) $type = 'array ';
else if ( $parameterClass = $parameter->getClass() ) $type = '''' . $parameterClass->getName() . ' ';
else $type = '';
$params[] = "$type '${$parameter->getName()}";
}
$paramString = implode( ',', $params );
$php[] = <<<EOT
private '${$methodName}Calls = [];
public function $methodName($paramString) {
'$this->{$methodName}Calls[] = func_get_args();
return '$this->$methodName;
}
EOT;
}
$php[] = '}';
$toEval = implode( "'n'n", $php );
eval( $toEval );
}
}