docteurklein / test-double-bundle
使用 symfony DIC 轻松创建测试替身
Requires
- php: >=5.4
- phpspec/prophecy: ^1.6
- symfony/dependency-injection: ^2.0|^3.0
- symfony/http-kernel: ^2.0|^3.0
This package is auto-updated.
Last update: 2024-08-29 04:14:31 UTC
README
什么?
一个 symfony 扩展包,简化测试替身创建。
使用 DIC 标签,你可以自动用存根或伪造替换服务。
为什么?
为了提高测试的隔离性,增加测试用例的精确性和变化。
通常,我们的 behat 套件使用来自数据库存根的真实数据。
这迫使我们创建全局、通用、适用于所有情况的存根。
真实的数据库还意味着在每个场景之前重置状态。
这个过程很慢,只是为了解决隔离性问题的一个权宜之计。
一个理想的测试套件将使用内存存储库运行每个场景。
每个场景应定义在特定上下文下 SUS 的行为。
拥有全局隐式上下文(数据库存根)确实很难测试不同的情况。
一种解决方案是将你的存储库替换为存根。
每个场景仅配置所需的工作存根。
注意:存根数据在进程间不可持久,因此不适合像典型的 mink+behat 套件那样的端到端测试。
但是,现在存储库已经进行了双重配置,你怎么知道你的真实存储库是否仍然工作呢?
嗯,这就是基础设施测试的作用。只有它们才会针对真实的后端运行,无论是存储库的数据库,还是 http 客户端的服务器。
要访问真实的服务,只需使用 <original-id>.real
。
通过这样做,理论上你就有良好的覆盖率、隔离性、速度
并且可以更好地捕捉回归的源头。
同时应用 通过示例建模。
如何?
安装
composer require docteurklein/test-double-bundle --dev
注册扩展包
public function registerBundles() { $bundles = [ new \DocteurKlein\TestDoubleBundle, // … ]; return $bundles; }
注意:你可能只想在测试环境中添加此扩展包。
集成 behat
此方法与 Symfony2Extension 集成得很好。
然后你可以将服务及其/或预言注入到上下文类中。
你也可以注入容器并一次性访问所有服务。
示例
注意:以下示例使用 JmsDiExtraBundle 来简化代码。
存根
使用 prophecy 创建存根。
注意:如果你没有提供任何标签属性,则会创建一个存根。如果没有给
stub
属性提供类或接口,则将创建服务类的存根。存根类不能是最终的。
- 首先,为服务定义一个存根 DIC 标签
/** * @Service("github_client") * @Tag("test_double", attributes={"stub"="GithubClient"}) */ final class GuzzleClient implements GithubClient { public function addIssue(Issue $issue) { // … } }
- 自动地,原始的
github_client
服务被替换为github_client.stub
服务。
为了控制此存根,你必须使用 github_client.prophecy
服务
$issue = new Issue('test'); $container->get('github_client.prophecy')->addIssue($issue)->willReturn(true);
伪造
注意:伪造实际上是 DIC 别名。
假设你有一个想要双重配置的服务。
- 首先,创建此服务并添加一个带有对应伪造服务的标签
/** * @Service("github_client") * @Tag("test_double", attributes={"fake"="github_client.fake"}) */ final class GuzzleClient implements GithubClient { public function addIssue(Issue $issue) { // … } }
- 然后,创建一个伪造实现并将其注册到伪造 ID
/** * @Service("github_client.fake") */ final class FakeClient implements GithubClient { public function addIssue(Issue $issue) { // … } }
Behat
注意:我们将
repo.invoices
和http.client
标记为 存根。
class Domain implements Context { public function __construct($container) { $this->container = $container; } /** * @Given a building in "maintenance mode" */ public function aBuildingInMaintenanceMode() { $this->building = new Building('BUILDING1337'); $this->building->putInMaintenanceMode(); } /** * @When its last unpaid invoice is being paid */ public function itsLastUnpaidInvoiceIsBeingPaid() { $this->container ->get('repo.invoices.prophecy') ->findOneByReference('UNPAID04') ->willReturn(Invoice::ownedBy($this->building)) ; $pay = $this->container->get('app.task.invoice.pay'); $pay('UNPAID04'); } /** * @Then it should be removed from maintenance mode */ public function itShouldBeRemovedFromMaintenanceMode() { $this->container ->get('http.client.prophecy') ->removeFromMaintenanceMode('BUILDING1337') ->shouldHaveBeenCalled() ; $this->container->get('stub.prophet')->checkPredictions(); } }