如何在使用Symfony2和Doctrine的Behat 3功能测试中回滚提交?

14
作为标题所示,我的目标是回滚在Behat功能测试期间进行的任何提交。 我检查了这个非常相似的答案,但它是两年前的,而且似乎不可能实现。
也许现在使用Behat 3就可以了。
我知道使用PHPUnit,我可以使用startUp和tearDown方法达到类似的效果。
我尝试使用@BeforeScenario和@AfterScenario注释钩住开始和回滚事务,但似乎behat和应用程序没有共享相同的实体管理器实例。
有什么建议吗?
谢谢。
更新
感谢您所有人的建议。以下是一些新的考虑:
- 加载固件:是的,它有效。我能够在我的测试开始之前运行固件,但问题(我的问题没有提到)是固件有时需要几分钟才能加载,等待10分钟或更长时间很麻烦。 - BEGIN / ROLLBACK TRANSACTION:它也有效或似乎有效。我没有收到错误,但测试期间编写的数据仍然存在于我的数据库中。我在一个标记有@BeforeScenario的方法中添加了第一个,在一个标记有@AfterScenario的方法中添加了后者。
$this->kernel->getContainer()
    ->get('doctrine.orm.entity_manager')
    ->getConnection()
    ->beginTransaction();

$this->kernel->getContainer()
   ->get('doctrine.orm.entity_manager')
   ->getConnection()
   ->rollBack();
- SAVEPOINT:我认为这正是我所需要的,但我的数据仍然存在。我尝试在我的@BeforeScenario方法中添加保存点的创建,在我的@AfterScenario方法中添加回滚。```php // 获取实体管理器的数据库连接对象 $connection = $this->kernel->getContainer()->get('doctrine.orm.entity_manager')->getConnection(); // 开启一个事务 $connection->beginTransaction(); // 在当前事务中创建一个保存点,名称为"tests" $connection->createSavepoint('tests'); ```
```php public function rollback(AfterScenarioScope $scope) { // 获取实体管理器的数据库连接对象 $connection = $this->kernel->getContainer()->get('doctrine.orm.entity_manager')->getConnection(); // 回滚到之前创建的名为"tests"的保存点 $connection->rollbackSavepoint('tests'); } ```
这些测试用例是用于测试我的REST API项目。考虑到这些情况,我认为Behat和我的应用程序在测试期间没有共享相同的实体管理器实例。您能否在测试期间将完全相同的实例在测试和项目之间共享?

1
好问题!我目前正在阅读一本关于Cucumber的书,这种技术在书中有所描述。我之前也考虑过它,但因为时间不够而放弃了。如果这是可能的,我很乐意知道,那我们就开始悬赏吧! - greg0ire
有一个叫做DoctrineFixturesBundle的东西,你可以从中受益。如果你修改了数据库中的数据,你可以重新加载fixtures,这样你的数据库就会重置为之前的状态。Before/After Feature/Scenario. 访问 http://www.inanzzz.com 并搜索 Fixtures,你会找到许多例子。除此之外,还有许多Behat的例子。 - BentCoder
你正在测试你的API rest,这使得回答变得不可能,因为只有在使用相同的数据库连接进行所有操作时,事务才能正常工作。在你的情况下,我认为你别无选择,只能重置你的模式。 - greg0ire
4个回答

7

如果您的上下文实现了KernelAwareContext,则在@BeforeScenario和@AfterScenario注释方法中,您可以执行以下操作:

$this->kernel->getContainer()->getDoctrine()->getConnection()->beginTransaction();
$this->kernel->getContainer()->getDoctrine()->getConnection()->rollBack();

假设您只有一个连接,并且它由em使用。
您还可以尝试使用$connection->setRollbackOnly(),但请记住,这将大大取决于您的底层数据库。MySQL可能会在您意想不到的情况下自动提交。
最后,还有$connection->createSavepoint('savePointName')可与$connection->rollbackSavepoint('savePointName')一起使用。
这是我的初步想法,可能需要一些调整。

谢谢你,Mike。我已经根据你的建议更新了我的问题。你还有其他好的想法吗? - stuzzo
另一种流行的方法是使用固定装置填充SQLite数据库,然后复制它。在设置/ beforeStep 方法中,您可以用干净的版本替换修改后的数据库。 - Mike Simonson
我可以确认交易解决方案有效!赏金已发放! - greg0ire
3
我是否可以建议您更新此答案,以展示开始事务和/或回滚它的完整注释方法的一些示例代码? - caponica

1
问题在于Behat使用了来自Browser-Kit的客户端,因此您会遇到可重启客户端的问题。 幸运的是,Symfony2扩展从容器中获取客户端,因此我们可以覆盖它。以下是我使用的技巧:
创建一个包装类:
class NoneRebootableClient extends Client
{
    public function __construct(KernelInterface $kernel, array $server = array(), History $history = null, CookieJar $cookieJar = null)
    {
        parent::__construct($kernel, $server, $history, $cookieJar);
        $this->disableReboot();
    }

    public function setServerParameters(array $parameters)
    {
        return;
    }
}

setServerParameters覆盖只有在您需要像“在每个场景之前设置身份验证标头”这样的东西时才需要。由于Behat在vendor/behatch/contexts/src/HttpCall/Request/BrowserKit.php:resetHttpHeaders中在每次调用后重置标头,因此我们将在第一次调用后失去身份验证标头。 注意:这可能会产生意外的副作用。

然后配置测试环境的服务(test.client id很重要,它是Behat用于客户端查找的id):

test.client:
    class: App\Tests\NoneRebootableClient
    public: true
    arguments:
        - '@kernel'
        - []
        - '@test.client.history'
        - '@test.client.cookiejar'

然后像您之前描述的那样,在您的上下文中使用Before/After Scenario:

/**
 * @BeforeScenario
 */
public function startTransaction($event)
{
    $this->doctrine->getConnection()->beginTransaction();
}

/**
 * @AfterScenario
 */
public function rollbackTransaction($event)
{
    $this->doctrine->getConnection()->rollBack();
}

Et voilà,我们是幂等的 :)


0

我认为“删除”和“创建”模式是你正在寻找的解决方案。

我正在使用Behat3进行功能测试 - 我正在测试相当复杂的Web应用程序和REST API。我使用准备好的固定装置,并在场景期间添加额外的数据。

您可以设置Behat上下文以为每个(之前)场景加载固定装置 - 这非常好用:

class CustomContext implements Context, KernelAwareContext {

    /**
     * @param type ScenarioEvent
     *
     * @BeforeScenario
     */
    public function reloadSchema($event)
    {
        // Note: EntityManager and ClassMetadata is required
        // reload Schema
        $schemaTool = new SchemaTool($entityManager);
        $schemaTool->dropSchema($metadata);
        $schemaTool->createSchema($metadata);
    }

    /**
     * @param type ScenarioEvent
     *
     * @AfterScenario
     */
    public function closeConnections($event)
    {
        // close connection(s)
    }

    // ...

}

在每个场景之前,Doctrine2都会删除并创建模式。接下来,通过以下捆绑包,您可以为您的场景加载特定/常见的夹具。

我正在使用以下配置来进行Behat夹具:

由此,您可以在场景中加载夹具:

Given the fixtures file "dummy.yml" is loaded
Given the fixtures file "dummy.yml" is loaded with the persister "doctrine.orm.entity_manager"
Given the following fixtures files are loaded:
  | fixtures1.yml |
  | fixtures2.yml |

如果您需要更多细节,请告诉我。


我认为你有点跑题了。这大致是我在上一个项目中使用的(我使用knplabs/friendly-contexts),但它很慢,因此需要回滚。 - greg0ire
好的,我不知道这个。 - Kamil Adryjanek

0

使用setUp方法,您可以开始一个事务。如果在两个方法调用之间没有提交,则可以在tearDown方法中回滚它。

即使您回滚查询,将测试启动到生产数据库上也非常危险。更好的方法是使用数据夹具初始化数据库测试。如果您无法这样做(我认为是这样),则希望使用生产数据进行测试。使用doctrine:migrations(或doctrine:schema:create)将您的生产数据库架构复制到开发环境中,并添加一个脚本来复制数据。


谢谢您的建议。现在我在开发环境中,我的特殊需求是每次启动测试时都不要丢弃和重新创建数据库架构并加载测试数据以节省时间。 - stuzzo
如果您使用 php app/console doctrine:fixtures:load -n 命令,您就不必删除和创建数据库,但在数据加载过程中,您仍然需要等待。 在您的功能测试中,您可以创建一个实体,启动测试并删除它。因此,在重新加载之前,您的测试可以被多次启动。 在我的固定装置中,我加载了大量用户,因此我可以多次启动我的删除、编辑测试。为了选择一个用户,我处理最后创建的用户,所以对于每个启动的测试,我都有一个实体。它会改变,但我有一个。 - Alexandre Tranchant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接