talentrydev / prophecy
针对PHP 5.3+的高度可定制的模拟框架
Requires
- php: ^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*
- doctrine/instantiator: ^1.2 || ^2.0
- phpdocumentor/reflection-docblock: ^5.2
- sebastian/comparator: ^3.0 || ^4.0 || ^5.0
- sebastian/recursion-context: ^3.0 || ^4.0 || ^5.0
Requires (Dev)
- phpspec/phpspec: ^6.0 || ^7.0
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^8.0 || ^9.0 || ^10.0
This package is auto-updated.
Last update: 2024-08-30 01:38:06 UTC
README
Prophecy是一个高度可定制且非常强大灵活的PHP对象模拟框架。虽然最初它是为了满足phpspec2的需求而创建的,但它足够灵活,可以与任何测试框架一起使用,只需付出最小的努力。
简单示例
<?php class UserTest extends PHPUnit\Framework\TestCase { private $prophet; public function testPasswordHashing() { $hasher = $this->prophet->prophesize('App\Security\Hasher'); $user = new App\Entity\User($hasher->reveal()); $hasher->generateHash($user, 'qwerty')->willReturn('hashed_pass'); $user->setPassword('qwerty'); $this->assertEquals('hashed_pass', $user->getPassword()); } protected function setUp() { $this->prophet = new \Prophecy\Prophet; } protected function tearDown() { $this->prophet->checkPredictions(); } }
安装
先决条件
Prophecy需要PHP 7.2.0或更高版本。
通过Composer进行设置
首先,将Prophecy添加到您的composer.json
文件中的依赖列表中
{ "require-dev": { "phpspec/prophecy": "~1.0" } }
然后只需使用Composer安装它
$> composer install --prefer-dist
您可以在其官方网页上了解更多关于Composer的信息。
如何使用它
首先,在Prophecy中,每个词都有逻辑意义,甚至库本身的名称(Prophecy)。当你开始感受到这一点时,你会非常熟练地使用这个工具。
例如,Prophecy之所以被命名为这样,是因为它专注于用非常有限的知识来描述对象未来的行为。但就像任何其他的预言一样,这些对象预言不能自己创造——应该有一个先知。
$prophet = new Prophecy\Prophet;
先知通过预言来创建预言
$prophecy = $prophet->prophesize();
prophesize()
方法调用的结果是新的ObjectProphecy
类对象。是的,那是你特定的对象预言,它描述了你的对象在不久的将来会如何行为。但首先,你需要指定你正在谈论哪个对象,对吧?
$prophecy->willExtend('stdClass'); $prophecy->willImplement('SessionHandlerInterface');
有两个有趣的调用——willExtend
和willImplement
。第一个告诉对象预言我们的对象应该扩展一个特定的类。第二个表示它应该实现一些接口。显然,PHP中的对象可以实现多个接口,但只能扩展一个父类。
虚构者
好的,现在我们有了我们的对象预言。我们能用它做什么?首先,我们可以通过揭示它的预言来获得我们的对象虚构者
$dummy = $prophecy->reveal();
现在,$dummy
变量持有了一个特殊的虚构对象。虚构对象是扩展和/或实现预设类/接口的对象,通过覆盖所有它们的公共方法。虚构的关键点是它们不包含任何逻辑——它们只是什么都不做。任何虚构的方法都将始终返回null
,虚构者永远不会抛出任何异常。如果你不关心这个替身的行为,只需要一个令牌对象来满足方法类型提示,虚构者就是你的朋友。
你需要理解一件事——虚构者不是预言。你的对象预言仍然分配给$prophecy
变量,为了操作你的期望,你应该使用它。$dummy
是一个虚构者——一个简单的php对象,它试图满足你的预言。
存根
好的,现在我们知道如何创建基本的预言并从中揭示虚构者。如果我们不关心我们的替身(反映原对象的对象)之间的交互,那就太棒了。如果我们关心,我们需要使用存根或模拟。
存根是一个对象替身,它对对象的行为没有期望,但当置于特定的环境中时,它会以特定的方式行为。好的,我知道这很神秘,但请再忍受一分钟。简单来说,存根是一个虚构者,它根据调用方法的签名做不同的事情(有逻辑)。在Prophecy中创建存根
$prophecy->read('123')->willReturn('value');
哇哦。我们刚刚对一个对象预言做了任意调用吗?是的,我们确实这样做了。这次调用返回了一个新的对象实例,属于类MethodProphecy
。没错,这是一个特定方法及参数的预言。方法预言使您能够创建方法承诺或预测。我们将在模拟部分后面讨论方法预测。
承诺
承诺是逻辑块,在预言术语中表示您的虚构方法,并由MethodProphecy::will(PromiseInterface $promise)
方法处理。实际上,我们之前所做的调用(willReturn('value')
)只是一个简单的快捷方式
$prophecy->read('123')->will(new Prophecy\Promise\ReturnPromise(array('value')));
这个承诺将导致对双倍read()
方法的调用,当且仅当有且仅有一个参数'123'
时,总是返回'value'
。但这仅适用于此承诺,还有许多其他承诺可以使用
ReturnPromise
或->willReturn(1)
- 从方法调用返回值ReturnArgumentPromise
或->willReturnArgument($index)
- 返回调用中的第n个方法参数ThrowPromise
或->willThrow($exception)
- 使方法抛出特定的异常CallbackPromise
或->will($callback)
- 提供了一种快速定义您自己的自定义逻辑的方法
请注意,您可以通过实现Prophecy\Promise\PromiseInterface
来添加更多的承诺。
方法预言的幂等性
预言强制执行相同的方法预言,因此对具有相同参数的相同方法调用具有相同的承诺和预测。这意味着
$methodProphecy1 = $prophecy->read('123'); $methodProphecy2 = $prophecy->read('123'); $methodProphecy3 = $prophecy->read('321'); $methodProphecy1 === $methodProphecy2; $methodProphecy1 !== $methodProphecy3;
这很有趣,对吧?现在您可能会问我如何定义更复杂的行为,其中某些方法调用改变其他方法的行为。在PHPUnit或Mockery中,您通过预测方法将被调用多少次来完成此操作。在Prophecy中,您将使用承诺来完成此操作
$user->getName()->willReturn(null); // For PHP 5.4 $user->setName('everzet')->will(function () { $this->getName()->willReturn('everzet'); }); // For PHP 5.3 $user->setName('everzet')->will(function ($args, $user) { $user->getName()->willReturn('everzet'); }); // Or $user->setName('everzet')->will(function ($args) use ($user) { $user->getName()->willReturn('everzet'); });
现在,无论方法调用多少次或以何种顺序调用,都没有关系。重要的是它们的行为以及您模拟的程度。
注意:如果方法被多次调用,您可以使用以下语法为每次调用返回不同的值
$prophecy->read('123')->willReturn(1, 2, 3);
实际上,这个功能不建议在大多数情况下使用。依赖于相同参数的调用顺序往往会使测试变得脆弱,因为添加一个额外的调用可能会破坏一切。
参数通配符
前面的例子很棒(至少我希望它对您来说很棒),但这还不够优化。我们在预期中硬编码了'everzet'
。难道没有更好的方法吗?事实上,确实有,但这需要了解这个'everzet'
实际上是什么。
您看,即使在创建方法预言时使用的方法参数看起来像是简单的参数,但实际上它们不是。它们是参数通配符。事实上,->setName('everzet')
看起来像是一个简单的调用,因为预言在底层自动将其转换为
$user->setName(new Prophecy\Argument\Token\ExactValueToken('everzet'));
这些参数通配符是简单的PHP类,实现了Prophecy\Argument\Token\TokenInterface
,并告诉预言如何比较实际参数与您的预期。是的,这些类名很大。这就是为什么存在一个快捷类Prophecy\Argument
,您可以使用它来创建这样的令牌
use Prophecy\Argument; $user->setName(Argument::exact('everzet'));
ExactValueToken
在我们的情况下不太有用,因为它迫使我们必须硬编码用户名。这就是为什么预言附带了一堆其他令牌
IdenticalValueToken
或Argument::is($value)
- 检查参数是否与特定值相同ExactValueToken
或Argument::exact($value)
- 检查参数是否匹配特定值TypeToken
或Argument::type($typeOrClass)
- 检查参数是否匹配特定类型或类名ObjectStateToken
或Argument::which($method, $value)
- 检查参数方法返回的值是否特定CallbackToken
或Argument::that(callback)
- 检查参数是否匹配自定义回调AnyValueToken
或Argument::any()
- 匹配任何参数- 任何值令牌(
AnyValuesToken
)或Argument::cetera()
- 匹配签名其余部分的任何参数 - 字符串包含令牌(
StringContainsToken
)或Argument::containingString($value)
- 检查参数是否包含特定的字符串值 - 数组包含令牌(
InArrayToken
)或Argument::in($array)
- 检查值是否在数组中 - 数组不包含令牌(
NotInArrayToken
)或Argument::notIn($array)
- 检查值是否不在数组中
您还可以通过实现自己的自定义类来添加更多,使用 TokenInterface
现在,让我们用参数令牌重构我们的初始 {set,get}Name()
逻辑
use Prophecy\Argument; $user->getName()->willReturn(null); // For PHP 5.4 $user->setName(Argument::type('string'))->will(function ($args) { $this->getName()->willReturn($args[0]); }); // For PHP 5.3 $user->setName(Argument::type('string'))->will(function ($args, $user) { $user->getName()->willReturn($args[0]); }); // Or $user->setName(Argument::type('string'))->will(function ($args) use ($user) { $user->getName()->willReturn($args[0]); });
就是这样。现在我们的 {set,get}Name()
预测将能够处理提供的任何字符串参数。我们刚刚描述了存根对象应该如何表现,即使原始对象可能完全没有行为。
关于参数的最后一点。您可能会问,在什么情况下
use Prophecy\Argument; $user->getName()->willReturn(null); // For PHP 5.4 $user->setName(Argument::type('string'))->will(function ($args) { $this->getName()->willReturn($args[0]); }); // For PHP 5.3 $user->setName(Argument::type('string'))->will(function ($args, $user) { $user->getName()->willReturn($args[0]); }); // Or $user->setName(Argument::type('string'))->will(function ($args) use ($user) { $user->getName()->willReturn($args[0]); }); $user->setName(Argument::any())->will(function () { });
什么也没有。您的存根将继续以之前的方式表现。这是因为参数通配符的工作方式。每个参数令牌类型都有不同的分数级别,通配符随后使用这些分数级别来计算最终参数匹配分数,并使用具有最高分数的方法预测承诺。在这种情况下,Argument::type()
在成功时得分为 5
,而 Argument::any()
得分为 3
。因此,类型令牌获胜,以及第一个 setName()
方法预测及其承诺。一个简单的规则 - 更精确的令牌总是获胜。
获取存根对象
好,现在我们知道了如何定义我们的预测方法承诺,让我们从它获取存根
$stub = $prophecy->reveal();
如您所见,获取模拟和存根之间的唯一区别是,对于存根,我们描述了每个对象的对话,而不是仅仅同意返回 null
(对象是 模拟)。实际上,在您定义了第一个承诺(方法调用)之后,预测将强制您定义所有通信 - 它在调用存根之前抛出 UnexpectedCallException
,以任何未使用对象预测描述的调用。
模拟
现在我们知道如何定义没有行为(模拟)和有行为但没有期望(存根)的模拟。剩下的就是有某些期望的模拟。这些称为模拟,在预测中它们看起来几乎与存根完全相同,除了它们在方法预测上定义的是 预测 而不是 承诺
$entityManager->flush()->shouldBeCalled();
预测
这里的 shouldBeCalled()
方法将 CallPrediction
分配给我们的方法预测。预测是您预测的延迟行为检查。您可以看到,在整个模拟生命周期中,预测记录了您在代码中对它进行的每个调用。之后,预测可以使用收集到的信息来检查它是否与定义的预测匹配。您可以使用 MethodProphecy::should(PredictionInterface $prediction)
方法将预测分配给方法预测。实际上,我们之前使用的 shouldBeCalled()
方法只是一个到
$entityManager->flush()->should(new Prophecy\Prediction\CallPrediction());
它检查您的目标方法(匹配方法名称和参数通配符)是否被调用 1 次或更多次。如果预测失败,则抛出异常。何时进行此检查?在您对主预言家对象调用 checkPredictions()
时。如果未定义任何预测,则它不会做任何事情。因此,在每次测试之后调用它不会有任何害处。
$prophet->checkPredictions();
在 PHPUnit 中,您会希望将此调用放入 tearDown()
方法中。如果没有定义任何预测,它将不会执行任何操作。所以每次测试之后调用它不会有任何害处。
还有许多其他预测可以与之交互
- 调用预测(
CallPrediction
)或shouldBeCalled()
- 检查方法是否被调用 1 次或更多次 - 未调用预测(
NoCallsPrediction
)或shouldNotBeCalled()
- 检查方法是否没有被调用 - 调用次数预测(
CallTimesPrediction
)或shouldBeCalledTimes($count)
- 检查方法是否被调用$count
次数 - 回调预测(
CallbackPrediction
)或should($callback)
- 检查方法是否满足您自己的自定义回调
当然,您可以通过实现 PredictionInterface
在任何时间创建自己的自定义预测。
间谍
预言中最酷的功能之一是即插即用的间谍支持。正如我在上一节所说,预言记录了双倍生命期内发生的每一个调用。这意味着您无需记录预测就可以进行检查。您还可以通过使用MethodProphecy::shouldHave(PredictionInterface $prediction)
方法手动完成。
$em = $prophet->prophesize('Doctrine\ORM\EntityManager'); $controller->createUser($em->reveal()); $em->flush()->shouldHaveBeenCalled();
对双倍进行此类操作称为间谍行为。而在预言中,这只是一个简单的工作。
常见问题解答(FAQ)
我可以在预言类上调用原始方法吗?
预言不支持在预言类上调用原始方法。如果您发现自己需要在调用其他方法的原始版本的同时模拟某些方法,这可能是您的类违反了单一职责原则的迹象,应该进行重构。