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!

  • 2017-07-26 Diego Aguiar

    Hey Gianni Obiglio

    Yes exactly, you first generate your test.db file with everything you would need, then make a backup of it, so before running next scenario you can restore it

    Also you might want to give a glance to this bundle https://github.com/liip/Lii...
    it can be configured to work with SQLite and it allows you to load only the fixtures you need

    Cheers!

  • 2017-07-26 Gianni Obiglio

    Hi, i am in a middle of isolating each of my test with a SQlite database, could you elaborate on "then copy this and use it as your database before each test" ? my current solution is load fixture just one time in a sqlite db, for exemple test.db, then i copy this file into a test_main.db, and after each test, to "restore my database" i replace test.db content by test_main.db content, is that what you had in mind ?

  • 2017-07-11 weaverryan

    Excellent link! Yes, lookup tables DO have a use-case - and that's a GREAT response describing when that is the case. They seem to be over-used, which is the reason for my opinion. In systems like Magento or Drupal, where so many things need to be able to expand and hook into logic and the database, they make a lot more sense :).

    Cheers!

  • 2017-07-10 thedotwriter

    Thanks you two, that's one thorough answer : ).

    I did not dig that much deeper into the subject but for people that might read this, lookup tables are still useful in some cases, just need to know when it's appropriate:

    1. Where you have a finite, yet expandable set of data in a column
    2. Where it isn't self describing
    3. To avoid data modification anomalies
    source: https://dba.stackexchange.c...

    That's mostly for point 3 that I was asking the question (and because I usually work with tools like Drupal or Magento which take care of creating complex database schemas themselves so I'm not used to doing it myself and I suck at it!).

    Back to learning Behat now...

  • 2017-07-05 weaverryan

    And to add a bit more:

    * typically you write code that references the specific values in a lookup table... so why not just put everything in code?
    * because of this, you usually can't just add a new row to a lookup table in the database and expect something to happen. You also need to push code that uses that new value (so again, keeping it in code would just make that simpler)
    * I don't like the idea of having a situation where if I delete a row in a table, the entire app breaks (because code is looking for a specific value in the lookup table that doesn't exist).

    So.... it adds some complexity... and I don't often see any benefit :). Good question though!

  • 2017-07-03 Victor Bocharsky

    Hey thedotwriter ,

    Because of extra joins. For example, you can have a User table and you need to store a subscription status, i.e. is user subscription "active", "canceled", "past_due", etc. So you can create a lookup table "subscription_status" and fill it in with those statuses. but in User table you will have something like:


    id | username | subscription_status_id |
    1 | edgar | 3 |
    2 | victor | 1 |


    So it's not so readable in the database, i.e. you have no idea what is the 3 or 1 IDs mean until you find them in subscritpion_status table. And if you want to fetch the actual status name - you need to perform an extra join, i.e. "User JOIN subscription_status".

    So the next example is much readable in DB and avoid any extra joins:


    id | username | subscription_status |
    1 | edgar | canceled |
    2 | victor | active |

    And in your code you can create a class constants to help referring those statuses:


    User {
    const SUBSCRIPTION_STATUS_ACTIVE = 'active';
    const SUBSCRIPTION_STATUS_CANCELED = 'canceled';
    }

    Cheers!

  • 2017-07-02 thedotwriter

    Hey, can you tell me why you hate lookup tables? I'd love to know. Is their any kind of design flaw with this practice?

    Cheers

  • 2016-12-08 Victor Bocharsky

    Hey Adam,

    I suppose you can get container with "$em = self::$container->get('doctrine')->getManager();" as in example here: https://knpuniversity.com/s... . The `self` keywords actually the same as `$this` but for a static context. But I think keeping ability to call "$this->getContainer()->get('doctrine')->getManager();" was the reason why we don't make this method static.

    Cheers!

  • 2016-12-07 Adam

    for now I have used @beforeScenario instead of @beforeSuite

  • 2016-12-07 Adam

    Suite hook callback: FeatureContext::clearData() must be a static method
    Making it a static method then means that $this is not available.
    How do we then get the container? as can not use $this->getContainer()->get('doctrine')->getManager();

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