How to rollback commits in Behat 3 functional tests with Symfony2 and Doctrine?
Asked Answered
K

4

14

As the title, my goal is to rollback any commit made during Behat functional tests. I checked this answer very similar, but it is from two years ago and it seems impossible to do.

Maybe with Behat 3 now it's possible.

I know that with PHPUnit I can reach something like that using startUp and tearDown methods.

I tried to start and rollback a transaction hooking with the @BeforeScenario and @AfterScenario annotations, but it seems that behat and the application doesn't share the same instance of entity manager.

Some advice on it?

Thank you.


UPDATE

Thank you all for your advices. Here some new considerations:

  • LOAD FIXTURES: Yes, it works. I'm able to run fixtures before my tests starts, but the problem (my fault to not mention it) is that fixtures sometimes needs several minutes to load and it is annoying to wait 10 or more minutes before your tests starts.

  • BEGIN/ROLLBACK TRANSACTION: It works too or it seems to be. I receive no errors, but the data written during tests is still in my database when they ended. I added the first in a method tagged @BeforeScenario e the latter in a method tagged with @AfterScenario

$this->kernel->getContainer()
    ->get('doctrine.orm.entity_manager')
    ->getConnection()
    ->beginTransaction();

$this->kernel->getContainer()
   ->get('doctrine.orm.entity_manager')
   ->getConnection()
   ->rollBack();
  • SAVEPOINT: I think that's exactly what I need, but my data is still there. I tried to add the creation of the savepoint in my @BeforeScenario method and the rollback on my @AfterScenario method
public function gatherContexts(BeforeScenarioScope $scope) {
    $environment = $scope->getEnvironment();
    $connection = $this->kernel->getContainer()->get('doctrine.orm.entity_manager')->getConnection();
    $connection->beginTransaction();
    $connection->createSavepoint('tests');
}

public function rollback(AfterScenarioScope $scope) {
    $connection = $this->kernel->getContainer()->get('doctrine.orm.entity_manager')->getConnection();
    $connection->rollbackSavepoint('tests');
}

All these tests are used to test my API REST project. After these considerations I think that Behat and my application doesn't share the same instance of the entity manager. Are you able to share the same exactly instance between your tests and your projects during tests?

Kish answered 28/10, 2015 at 23:59 Comment(3)
Great question! I'm currently reading a book about Cucumber, and this technique is described in it. I had thought about it before, but dismissed it b/c lack of time. I'd be glad to know if this is possible, so let's start a bounty!Ene
There is something called DoctrineFixturesBundle which you can benefit from. If you modify data in DB, you can reload fixtures so that your DB is reset to what it was before. Before/After Feature/Scenario. Go to inanzzz.com and search Fixtures where you'll find many examples. On top of that, there are many Behat examples in there.Rath
The fact that you are testing your API rest makes this impossible to answer, b/c transactions will only work if you do everything with the same database connection. In your case, you have no choice but to reset your schema IMO.Ene
E
7

If your context implements the KernelAwareContext, then in the @BeforeScenario and @AfterScenario annotated methods, you can do

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

This is assuming you only have one connection and it's used by the em.

You can also try $connection->setRollbackOnly() but bear in mind that it will wildly depend on your underlying db. Mysql might autocommit in quite a few case were your didn't expected it.

And lastly there is also $connection->createSavepoint('savePointName') to use with $connection->rollbackSavepoint('savePointName')

This is out of my head so it might needs some adjustments.

Elsyelton answered 5/8, 2016 at 15:36 Comment(4)
Thank you Mike. I've updated my question with some of your advices. Do you have some other nice idea?Kish
Well another popular mehtod is just to populate an slqite db with the fixtures then make a copy of it. And in the setup/beforeStep method, you replace the modified version of the db with the clean one.Elsyelton
I can confirm the transaction solution works! Bounty awarded!Ene
Might I suggest that you update this answer with some example code to show the full annotated method which is beginning the transaction and/or rolling it back?Contestant
H
1

The problem is that Behat uses the client from the Browser-Kit, so you are suffering from rebootable client. Luckily the Symfony2 extension fetches the client from the container, so we can override it. Here is what did the trick for me:

Create a wrapper class:

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;
    }
}

The setServerParameters override is only required if you have something like "Before each scenario, set an auth header". As Behat resets the headers after each call in vendor/behatch/contexts/src/HttpCall/Request/BrowserKit.php:resetHttpHeaders we would loose the auth header after the first call. Note: this might have unexpected side-effects.

Then configure the service for the test-env (the test.client id is important, its the id that behat uses for the client lookup):

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

And then use the Before/After Scenario in your context as you already described above:

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

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

Et voilà, we are idempotent :)

Halvorson answered 15/11, 2018 at 13:52 Comment(0)
A
0

With setUp method, you can begin a transaction. You can rollback it in tearDown method IF there is no commit between the two method calls.

It's very dangerous to launch tests on a production database even if you rollback queries. It's a better way to initialize a database test with data fixture. If you can not do it (I think so), you want to test with production data. Use doctrine:migrations (or doctrine:schema:create) to copy your production database schema into your dev environnement and add a script to copy data.

Alurta answered 29/10, 2015 at 7:58 Comment(2)
Hi thank you for your advices. Now, I'm in dev environment and my special need is to not drop and recreate database schema and load the fixtures every time I launch tests in order to save time.Kish
If you use php app/console doctrine:fixtures:load -n you don't have to drop and create database, but you still have to wait during data loading process. During your functionnaly tests, you can create an entity, launch your test and delete it. So your tests can be launched more than one time before reloading. In my fixtures, I load an huge quantity of users, so I can launch my deletion, edition tests a lot of time. To select an user, I handle the last user created, so for each launched test, I had an entity. It change, but I have one.Alurta
P
0

I think that "dropping" and "creating" schema is a solution you are looking for.

I'm using Behat3 for my functional testing - I'm testing quite complex web application and REST APIs. I'm using prepared fixtures and also adding extra data during scenario.

You can setup Behat context to load fixtures for each (before) scenario - this is working really nice:

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)
    }

    // ...

}

Before each scenario Doctrine2 is dropping and creating schema. Next thanks to bundles below you can load specific / common fixtures for your scenarios.

I'm using following configuration for Behat fixtures:

Thanks to this you can load fixtures in scenarios:

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 |

Let me now if you need more details.

Postlude answered 9/8, 2016 at 12:47 Comment(2)
I think you are a bit off topic. This is roughly what I used on my last project (I'm using knplabs/friendly-contexts for that), and it is slow, hence the need for rollbacksEne
Ok, I didn't know about it.Postlude

© 2022 - 2024 — McMap. All rights reserved.