Buy

Controlling the Database

Eventually, you'll need to insert test data at the beginning of your scenarios. And that's when things get tricky. So, let's learn to do this right.

First: never assume that there is data in the database at the beginning of a scenario. Instead, you should put any data you need there intentionally with a Given.

Go to the top of this scenario and add:

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

I'm inventing this language - it describes something that needs to be setup using natural language. Change the Given line below this to And for readability:

14 lines features/web/authentication.feature
... lines 1 - 7
And I am on "/"
... lines 9 - 14

Run it again: it should print out the step definition for this new language.

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

And it does! Copy that and put it into our FeatureContext class. Change :arg1 to :username and :arg2 to :password. Update the arguments to match:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 41
/**
* @Given there is an admin user :username with password :password
*/
public function thereIsAnAdminUserWithPassword($username, $password)
{
... lines 47 - 54
}
... lines 56 - 87

To fill this in, we want to insert a new user in the database with this username and password. If Symfony were loaded, that would be really easy: I'd create a User object, set some data then persist and flush it. So let's do that! Even if you're not using Symfony, you'll use the same basic process to bootstrap your application to get access to all of your normal useful objects and functions.

Bootstrapping Symfony in FeatureContext

We only need to boot Symfony once at the beginning of the test suite. Afterwards all of our scenarios will be able to access Symfony's container. Make a new public function bootstrapSymfony():

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 31
public static function bootstrapSymfony()
{
... lines 34 - 39
}
... lines 41 - 87

And inside, we'll do exactly what its name says.

We'll need a couple of require statements for autoload.php and the AppKernel.php class:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 33
require_once __DIR__.'/../../app/autoload.php';
require_once __DIR__.'/../../app/AppKernel.php';
... lines 36 - 87

Then, it's as easy as $kernel = new AppKernel();. Pass it the environment - test - and the debug value - true - so we can see errors. Finish with $kernel->boot();:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 36
$kernel = new AppKernel('test', true);
$kernel->boot();
... lines 39 - 87

Congrats - you just bootstrapped your Symfony app.

What we really want is access to the service container. To get that, create a new private static $container; property:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 15
private static $container;
... lines 17 - 87

Then in the method, set that with self::$container = $kernel->getContainer();:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 38
self::$container = $kernel->getContainer();
... lines 40 - 87

Now, as long as we call bootstrapSymfony() first, we'll have access to the container. Oh, and update the method to be a public static function:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 31
public static function bootstrapSymfony()
... lines 33 - 87

I'm making this all static because it allows us to have one container across all of our different scenarios. Because remember, each scenario gets its own context instances.

We could call this method manually, but that's not fancy! Remember the hook system? We used @BeforeScenario and @AfterScenario before, but there are other hooks, like @BeforeSuite. Let's use that!

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 28
/**
* @BeforeSuite
*/
public static function bootstrapSymfony()
... lines 33 - 87

Behat will call this method one time, even if we're testing 10 features and 100 scenarios.

Saving a New User

Inside of thethereIsAnAdminUserWithPassword() step definition, let's go to work! I already have a User entity setup in the project, so we can say $user = new User(). Then set the username and the "plainPassword":

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 46
$user = new \AppBundle\Entity\User();
$user->setUsername($username);
$user->setPlainPassword($password);
... lines 50 - 87

I have a Doctrine listener already setup that will encode the password automatically. Which is good: it's well-known that raptors can smell un-encoded passwords...

In this app, to make this an "admin" user, we need to give the user ROLE_ADMIN:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 49
$user->setRoles(array('ROLE_ADMIN'));
... lines 51 - 87

Now the moment of truth: we need the entity manager. Cool! Grab it with $em = self::$container->get('doctrine') (to get the Doctrine service) and then ->getManager();:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 51
$em = self::$container->get('doctrine')->getManager();
... lines 53 - 87

It's easy from here: $em->persist($user); and $em->flush();:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 52
$em->persist($user);
$em->flush();
... lines 55 - 87

And that should do it!

We already have a user called admin in the database, so let's test this using admin2. Give it a go!

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

This should not work, since this user isn't in the database yet... unless it's created by the code we just wrote! Brilliant!

This is huge: we're guaranteeing there's an admin2 user by bootstrapping our app and being dangerous with all of our useful services.

Leave a comment!

  • 2016-02-17 weaverryan

    Hey Shaun!

    Actually, what you have will *definitely* work (as you know). What you don't see in my example is that (thanks to PhpStorm) as soon as I auto-complete the User class, it added a "use AppBundle\Entity\User" to the top of the class. So if you have that "use" statement, you can have the short version like mine. But, both work :).

    Cheers!

  • 2016-02-17 Shaun

    In order for the User class to be recognized I had to use "$user = new \AppBundle\Entity\User();"