Buy

The SymfonyExtension & Clearing Data Between Scenarios

Change the user and pass back to match the original user in the database: "admin" and "admin":

14 lines features/web/authentication.feature
... lines 1 - 6
Given there is an admin user "admin" with password "admin"
... lines 8 - 14

Now rerun the scenario:

./vendor/bin/behat features/web/authentication.feature

Boom! This time it explodes!

Integrity constraint violation: UNIQUE constraint failed: user.username

We already have a user called "admin" in the database... and since I made that a unique column, creating another user in Given is putting a stop to our party.

Clearing the Database Before each Scenario

Important point: you should start every scenario with a blank database. Well, that's not 100% true. What I want to say is: you should start every scenario with a predictable database. Some projects have look-up tables - like a "product status" table with rows for in stock, out of stock, back ordered, etc. I really hate these, but anyways, sometimes there are tables that need to be filled in for anything to work. You'll want to empty the database before each scenario... except for any lookup tables.

Since we don't have any of these pesky look-up guys, we can empty everything before every scenario. To do this, we'll of course, use hooks.

Create a new public function clearData():

97 lines features/bootstrap/FeatureContext.php
... lines 1 - 13
class FeatureContext extends RawMinkContext implements Context, SnippetAcceptingContext
{
... lines 16 - 44
public function clearData()
{
... lines 47 - 49
}
... lines 51 - 95
}

Clearing data now is pretty easy, since we have access to the entity manager via self::container->get('doctrine')->getManager();:

97 lines features/bootstrap/FeatureContext.php
... lines 1 - 46
$em = self::$container->get('doctrine')->getManager();
... lines 48 - 97

Now we can issue DELETE queries on the two entities that we care about so far: product and user. I'll use $em->createQuery('DELETE FROM AppBundle:Product')->execute();:

97 lines features/bootstrap/FeatureContext.php
... lines 1 - 47
$em->createQuery('DELETE FROM AppBundle:Product')->execute();
... lines 49 - 97

Copy and paste that line and change "Product" to "User":

97 lines features/bootstrap/FeatureContext.php
... lines 1 - 48
$em->createQuery('DELETE FROM AppBundle:User')->execute();
... lines 50 - 97

Oh and make sure that says "Product" and not "Products". Activate all of this with the @BeforeScenario annotation:

97 lines features/bootstrap/FeatureContext.php
... lines 1 - 41
/**
* @BeforeScenario
*/
public function clearData()
... lines 46 - 97

Try it all again:

./vendor/bin/behat features/web/authentication.feature

Perfect! We can run this over and over because it's clearing out the data first.

The Symfony2Extension

And, surprise! There's an easier way to bootstrap Symfony and clear out the database. I always like taking the long way first so we can see how things work.

First, install a new library called behat/symfony2-extension with --dev so it goes into my require dev section:

composer require behat/symfony2-extension --dev

An extension in Behat is a plugin. We're already using the MinkExtension:

19 lines behat.yml
default:
... lines 2 - 12
extensions:
Behat\MinkExtension:
base_url: http://localhost:8000
... lines 16 - 19

Activate the new plugin in behat.yml: Behat\Symfony2Extension::

20 lines behat.yml
default:
... lines 2 - 12
extensions:
Behat\MinkExtension:
... lines 15 - 18
Behat\Symfony2Extension: ~

And as luck would have it, it doesn't need any configuration. It looks like we still need to wait for it to finish installing in the terminal... there we go!

The most important thing the Symfony2 Extension gives you is, access to Symfony's container... but wait, we already have that? Well, this just makes it easier.

Remove the private static $container; property and the bootstrapSymfony() function. Instead of these, we'll use a PHP 5.4 trait called KernelDictionary:

84 lines features/bootstrap/FeatureContext.php
... lines 1 - 13
class FeatureContext extends RawMinkContext implements Context, SnippetAcceptingContext
{
use \Behat\Symfony2Extension\Context\KernelDictionary;
... lines 17 - 82
}

This gives us two new functions, getKernel(), but more importantly getContainer():

56 lines vendor/behat/symfony2-extension/src/Behat/Symfony2Extension/Context/KernelDictionary.php
... lines 1 - 21
trait KernelDictionary
{
... lines 24 - 40
public function getKernel()
{
return $this->kernel;
}
... lines 45 - 50
public function getContainer()
{
return $this->kernel->getContainer();
}
}

It takes care of all of the booting of the kernel stuff for us, and it even reboots the kernel between each scenario so they don't run into each other. That's important because remember, each scenario should be completely independent of the others.

Search for the old self::$container code. Change it to $this->getContainer():

84 lines features/bootstrap/FeatureContext.php
... lines 1 - 31
public function clearData()
{
$em = $this->getContainer()->get('doctrine')->getManager();
... lines 35 - 36
}
... lines 38 - 41
public function thereIsAnAdminUserWithPassword($username, $password)
{
... lines 44 - 48
$em = $this->getContainer()->get('doctrine')->getManager();
... lines 50 - 51
}
... lines 53 - 84

You see that PhpStorm all of a sudden auto-completes the methods on the services we fetch because it recognizes this as the container and so knows that this returns the entity manager.

Let's try things again!

./vendor/bin/behat features/web/authentication.feature

Still works! But now with less effort. If you have multiple context classes, you can use the KernelDictionary on all of them to get access to the container.

Clearing the Database Easily

OK, so what about clearing the database? It'll be a huge pain to add more and more manual queries. Fortunately Doctrine gives us a better way: a Purger. Create a new variable called $purger and set it to a new ORMPurger(). Pass it the entity manager:

84 lines features/bootstrap/FeatureContext.php
... lines 1 - 32
public function clearData()
{
$purger = new ORMPurger($this->getContainer()->get('doctrine')->getManager());
... line 36
}
... lines 38 - 84

After that, type $purger->purge();, and that's it:

84 lines features/bootstrap/FeatureContext.php
... lines 1 - 35
$purger->purge();
... lines 37 - 84

This will go through each entity and clear out all of your data. If it's working, then our tests should pass:

./vendor/bin/behat features/web/authentication.feature

And they do! Same functionality and a lot less code. For bigger databases with lots of lookup tables, it may be too much to clear every table and re-add all the data you need. In those cases, trying experimenting with creating a SQL file that populates the database and executing that before each scenario. Or, populate an SQLite file with whatever you want to start with, then copy this and use it as your database before each test. That's a super-fast way to roll back to your known data set.

Leave a comment!

  • 2016-11-22 Victor Bocharsky

    Yes, looks great! Thanks for sharing it

    Cheers!

  • 2016-11-21 Sergiu Popa

    Thanks. I'm posting here the final code:

    $em = $this->getContainer()->get('doctrine')->getManager();
    $em->getConnection()->exec('SET foreign_key_checks=0');
    $purger = new ORMPurger($this->getContainer()->get('doctrine')->getManager());
    $purger->purge();
    $em->getConnection()->exec('SET foreign_key_checks=1');

  • 2016-11-21 Victor Bocharsky

    Hey Sergiu,

    Yes, you're right, but DELETE queries might throw exception too due to foreign keys, so you should use the correct order of DELETE queries. One more hack here is SET foreign_key_checks=0 before DELETE queries and then set it back.

    Cheers!

  • 2016-11-20 Sergiu Popa

    Cleaning the database using DETELE queries is better sometimes, as the purges can throw error because of foreign constrains, e.g.: deleting categories before deleting articles_categories entries.