Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The SymfonyExtension & Clearing Data Between Scenarios

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

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

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

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

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

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

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

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

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

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

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

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

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

33
Login or Register to join the conversation
Soltan Avatar

Class AppKernel does not exist

Why am I have this error in console?

1 Reply

Hey Murad,

But do you have AppKernel class in your project? Does it have namespace?

Cheers!

Reply
Soltan Avatar

Hi Viktor,
How can I add AppKernel class to my project? Is it mentioned in tutorial? Thank you

Reply

Hey Soltan!

This tutorial was made for Symfony 3, which has a different directory structure than Symfony 4. In Symfony 4, the class is known as just "Kernel" and it lives in your src/directory. A few things will work differently if you're using Behat on Symfony 4. But, we're happy to answer any questions.

Cheers!

Reply
Soltan Avatar
Soltan Avatar Soltan | weaverryan | posted 4 years ago | edited

Hi @weaverryan

Could you add some instructions about configuration required to Symfony 4?

Reply

As far as I know, the only piece of config that changed is the location of your AppKernel file, just adjust it and Behat should run without problems

Reply
MichalWilczynski Avatar
MichalWilczynski Avatar MichalWilczynski | posted 1 year ago

Hi,

In your blog post

https://symfonycasts.com/bl...

I read that that in symfony 5 we should use packages from the friends-of-behat organization.
So I installed all packages from friends-of-behat but according to this video I should use KernelDictionary trait but there is no KernelDictonary after I installed behat things from friends-of-behat. What should I do to bootstrap Symfony?.

Before update I used:
use Behat\Symfony2Extension\Context\KernelDictionary;

Reply

Hey MichalWilczynski

Sorry for late answer, with this new setup you just need to autowire, the key here is recipe https://github.com/symfony/... which allows to use autowiring for Context classes

1 Reply
MichalWilczynski Avatar
MichalWilczynski Avatar MichalWilczynski | sadikoff | posted 1 year ago | edited

Hey, thank you :) it does work but I one more problem.

Here is my <b>behat.yaml.dist</b>


default:
    extensions:
        FriendsOfBehat\SymfonyExtension: ~
        Behat\MinkExtension:
            base_url:  'http://test'
            default_session: symfony
            sessions:
                symfony:
                    symfony: ~
        SensioLabs\Behat\PageObjectExtension:
            namespaces:
                page: App\Tests\Behat\bootstrap\Features\Application\Web\Page

This is an illustrative class


class testContext extends PageObjectContext
{
   private $kernel;
   private $userRepository;
   private const $id = '1111-1111-1111-1111';

   public function __construct(KernelInterface $kernel, UserRepository $userRepository)
   {
      $this->kernel = $kernel;
      $this->userRepository = $userRepository;
   }

   protected function getContainer()
    {
        return $this->kernel->getContainer();
    }
  
     /**
     * @When I accept terms
     */
  public function IAcceptTerms($id)
  {
     $page = $this->getPage(TermsPage::class)->open();
     $page->findById('accept')->press();
  }



     /**
     * @Then terms should be accepted
     */
  public function ThenTermsShouldBeAccepted()
  {
     $user = $this->userRepository()->findById(self::MY_ID);
     if(!$user->isAcceptTerms) {
      throw new RuntimeExcepion('Terms not accepted!')
      }
 }
}

When I go to phpmyadmin I can see that there is acceptTerms set to true after firstStep but in the step ThenTermsShouldBeAccepted acceptTerms are set to false and exception is thrown. I dont'know from where is this difference between database and what userRepository returns.
Thanks in advance :)

Reply
Sergiu P. Avatar
Sergiu P. Avatar Sergiu P. | posted 4 years ago | edited

For Symfony 4.2 with Doctrine Fixtures 3.1:

<b>behat.yml.dist:</b>


default:
    suites:
        default:
            contexts:
                - FeatureContext:
                      kernel: '@kernel'
                      fixturesLoader: '@doctrine.fixtures.loader'
                - Behat\MinkExtension\Context\MinkContext

<b>features/bootstrap/FeatureContext.php</b>


public function __construct(KernelInterface $kernel, SymfonyFixturesLoader $fixturesLoader)
{
    $this->container = $kernel->getContainer();
    $this->fixturesLoader = $fixturesLoader;
}

/**
 * @BeforeScenario
 */
public function clearData(): void
{
        $purger = new ORMPurger($this->em);
        $executor = new ORMExecutor($this->em, $purger);
        $executor->execute($this->fixturesLoader->getFixtures());
}

For a login scenario to work, I also had to comment the session config lines in config/packages/test/frameworl.yaml:


framework:
        test: true
        #session:
                #storage_id: session.storage.mock_file

However, I'd like to load the fixtures @BeforeFeature or @BeforeSuite, but these are static methods. Any ideas?

Reply

Hey Sergiu P.!

With the approach you currently have (the Symfony2Exentension way of doing things. I don't think it's possible to do what you want. BeforeSuite or BeforeFeature because, by definition, need to be executed before your context class is instantiated and so need to be static. You would need to follow an approach that's more similar to what we do in this tutorial *before* introducing the Symfony2Extension: manually instantiate the kernel in a static method and set it on a static property. I do this all the time with integration tests in PhpUnit anyways - I think it's a fine approach. Honestly, I think the Symfony2Extension has less and less uses - as we don't build things in bundles anymore (so we don't need it to discover features in bundles) and instantiating a kernel to get the container is just an easy thing to do.

Let me know if that makes sense!

Cheers!

Reply
Sergiu P. Avatar
Sergiu P. Avatar Sergiu P. | weaverryan | posted 4 years ago | edited

Totally!


use App\Kernel;
use Behat\Behat\Context\Context;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;

class FeatureContext implements Context
{
    /** @var \Symfony\Component\DependencyInjection\ContainerInterface|null */
    private static $container;


    /**
     * @BeforeSuite
     */
    public static function bootstrapSymfony()
    {
        require_once __DIR__.'/../../features/bootstrap/bootstrap.php';
        require_once __DIR__.'/../../src/Kernel.php';


        $kernel = new Kernel('test', true);
        $kernel->boot();


        $container = $kernel->getContainer();
        static::$container = $container->has('test.service_container') ?
            $container->get('test.service_container') : $container;


        $em = self::$container->get('doctrine.orm.default_entity_manager');
        $fixturesLoader = self::$container->get('doctrine.fixtures.loader');


        $purger = new ORMPurger($em);
        $executor = new ORMExecutor($em, $purger);
        $executor->execute($fixturesLoader->getFixtures());
    }
}

The container has all the private services as public: https://symfony.com/blog/new-in-symfony-4-1-getting-container-parameters-as-a-service

Reply

This is great! Nice work and thanks for sharing!!!

Reply
Default user avatar
Default user avatar Julien Quintiao | posted 4 years ago | edited

Hi guys,

I'm trying to delete some entities in a custom function in my context.
Unfortunately, i have some troubles to delete a entity related to another one.

Basically, i have a first entity called Agent with an attribute :


/**
 * @ORM\ManyToOne(targetEntity="VRZ\Bundle\AccountBundle\Entity\Account", inversedBy="agents")
 * @ORM\JoinColumn(onDelete="SET NULL")
 */
private $account;

In my Context, using Symfony2Extension, i can get my agent like this :


$em = $this->getContainer()->get('doctrine.orm.entity_manager');
$agent = $em->getRepository('VRZAccountBundle:AccountAgent')->findOneBy(array('email' => $email));

When i try to get the account by Relationship, $agent->getAccount(), a proxyObject is returned :


Proxies\__CG__\VRZ\Bundle\AccountBundle\Entity\Account {#5344
  +__isInitialized__: false
  -id: 48

But when i try to delete those 2 entities, doctrine can't find the Account Entity and give this error :
Entity of type 'VRZ\Bundle\AccountBundle\Entity\Account' for IDs id(48) was not found (Doctrine\ORM\EntityNotFoundException)

This is the code of the function in my Context :


/**
 * @Given There is no email with :email
 */
public function thereIsNoEmailWith($email)
{

    $em = $this->getContainer()->get('doctrine.orm.entity_manager');
    $agent = $em->getRepository('VRZAccountBundle:AccountAgent')->findOneBy(array('email' => $email));
    dump($agent);

    if ($agent) {
        $account = $agent->getAccount();
        dump($account);
        if ($account) {
            $em->remove($account);
        }
        $em->remove($agent);
    }

    $em->flush();

}

Any ideas ?
Thank you !

Reply

Hey Julien Quintiao!

Let's see if we can figure this out :). A few things:

A) Don't worry about the Proxy object - that will look and act just like a normal Account object. If it does NOT, then it's probably a different issue.

B) Speaking of "a different issue", this error should never occur:

Entity of type 'VRZ\Bundle\AccountBundle\Entity\Account' for IDs id(48)

This basically means that the following happened:

1) You queried for some account AccountAgent and, in the database, this was linked to an Account object with id 48
2) You tried to access the Account from the AccountAgent (e.g. $agent->getAccount()->getId()), but suddenly, when Doctrine tried to query for the Account object with id 48 (because it only does this query lazily, when you actually need the data - and I think it also does the query when you try to delete), it was gone! That doesn't make any sense, right? You haven't actually deleted it yet - so it should be there!

Here's what I think is going on. Suppose you query for some object in Behat (e.g. an Account) that has (e.g.) a name of "foo". Now you execute a scenario that changes the Account's name to "Bar" by browsing to some form and filling in "Bar". After the scenario, if you run $account->getName(), what value will it be? foo or bar? It will most likely be "foo" (but it actually depends on how you're running your tests). The problem is that (unless you're using the symfony2extension as your driver for making the web requests), you query for the object in Behat, but when you make the web requests, those happen in a totally difference process (maybe even by opening a real browser), and so the Doctrine objects inside of your Behat process have no idea that data changed in the database. So, you get stale data. I believe that's what's happening to you (but I could be wrong and writing this wall of text for nothing!). You're querying for an AccountAgent (whose linked Account is 48), but it's somehow changed later by running a scenario, and then when you try to access it again in Behat, the id is now wrong.

The fix for this (and we DO do this in our app in real life) is to refresh the object when you think this is a problem. So, something like:


$em->refresh($agent);

That should be it! That will then go get fresh data and (hopefully) solve your issue.

But, let me know! Cheers!

Reply
Default user avatar
Default user avatar Julien Quintiao | weaverryan | posted 4 years ago | edited

Hi Ryan,
First of all, thank you for all your screencasts ! You tought me a lot with fun !

I found the reason of my issue. I was totally going crazy :D

The reason was this, at the top of my Account entity :


* @Gedmo\SoftDeleteable(fieldName="deletedAt")

The entity was not "really" deleted, but apparently i asked Doctrine to think it is...

My bad, i totally forgot about that annotation.
Thank you for your investigations :)

Keep up the good and fun work guys !
Cheers.

Reply

Ha! Glad you figured it out! And thanks for the nice words :). I both love and hate SoftDeleteable for this reason - it works too well ;)

Cheers!

Reply
Sergiu P. Avatar
Sergiu P. Avatar Sergiu P. | posted 4 years ago | edited

I'm trying to setup Behat for Symfony 4. I managed to clear the data between scenarios and it's working fine for the test database, but the requests are being made for DEV environment, even if I run in the terminal: APP_ENV=test vendor/bin/behat.

This is my behat.yml:


default:
    suites:
        default:
            contexts:
                - FeatureContext:
                      kernel: '@kernel'
                - Behat\MinkExtension\Context\MinkContext

    extensions:
        Behat\Symfony2Extension:
            kernel:
                bootstrap: features/bootstrap/bootstrap.php
                class: App\Kernel

        Behat\MinkExtension:
            base_url: http://expenses.local
            browser_name: chrome
            sessions:
                default:
                    goutte: ~
                javascript:
                    selenium2: ~

And my bootstrap.php inside features/bootstrap:


use Symfony\Component\Dotenv\Dotenv;

// The check is to ensure we don't use .env in production
if (!isset($_SERVER['APP_ENV'])) {
    if (!class_exists(Dotenv::class)) {
        throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
    }
    (new Dotenv())->load(__DIR__.'/.env');
}

Basically the .env file inside features/boostrap specified that the environment is test.

At the moment the only solution I found is to create index_test.php inside /public and use it in the base url.

Any thoughts?

Thank you

Reply

Hey Sergiu P.

I see your problem, the thing is that you are using Goutte's driver, what Goutte does is to make requests to your app using Guzzle (something very similar as opening a website in a browser), so you will be always hitting your front controller public/index.php and hence, you will hit the environment specified in your ".env" file. What you can do is to use behat/symfony2-extension library, but I just noticed that you also use "Selenium", so the problem will remain for that session.
I'm afraid that there is no other option than creating a test front controller as you just did, just don't forget to delete it after every deploy, or add some security checks for not allowing to be hitted from the outside (only your localhost)

Cheers!

Reply
Default user avatar
Default user avatar thedotwriter | posted 5 years ago

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

Reply

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!

Reply

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!

Reply
Default user avatar

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

Reply

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!

Reply
Sergiu P. Avatar
Sergiu P. Avatar Sergiu P. | posted 5 years ago

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.

Reply

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!

Reply
Sergiu P. Avatar

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');

Reply

Yes, looks great! Thanks for sharing it

Cheers!

Reply
Default user avatar
Default user avatar Gianni Obiglio | posted 5 years ago

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 ?

Reply

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!

Reply
Default user avatar

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

Reply
Default user avatar

for now I have used @beforeScenario instead of @beforeSuite

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a very old version of Symfony. The fundamentals of Behat are still valid, but integration with Symfony will be different.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.4.0, <7.3.0",
        "symfony/symfony": "^2.7", // v2.7.4
        "twig/twig": "^1.22", // v1.22.1
        "sensio/framework-extra-bundle": "^3.0", // v3.0.16
        "doctrine/doctrine-bundle": "^1.5", // v1.5.1
        "doctrine/orm": "^2.5", // v2.5.1
        "doctrine/doctrine-fixtures-bundle": "^2.2", // v2.2.1
        "behat/symfony2-extension": "^2.0" // v2.0.0
    },
    "require-dev": {
        "behat/mink-extension": "^2.0", // v2.0.1
        "behat/mink-goutte-driver": "^1.1", // v1.1.0
        "behat/mink-selenium2-driver": "^1.2", // v1.2.0
        "phpunit/phpunit": "^4.8" // 4.8.18
    }
}
userVoice