Buy Access to Course
14.

The SymfonyExtension & Clearing Data Between Scenarios

Share this awesome video!

|

Keep on Learning!

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.

Tip

Working with Symfony 5 or higher? Check out this blog post to get all you need

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():

// ... 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.