Teaching Your Database To Forget


In this video we are going to ensure our database is always ready to run any of our Behat tests. To do this, we need to ensure that our database exists, and also that the contents of the database - the tables - are reset before any further steps occur.

The way we will do this is by getting Behat to drop and recreate our database table schemas before any test scenario occurs.

Thankfully, Behat comes with in-built annotations which, when used, can hook into the Behat test process and allow us to do whatever we need to do before, or after, a test suite, scenario, or even an individual test step. You can read more about this in the official Behat manual.

The way in which we will use Behat's annotations is to use the @BeforeScenario hook to drop and recreate our table schemas before any scenario runs.

We will then populate (or, re-populate) our freshly reset database tables with all the data in our Feature background - which looks something like this:

  Background:
    Given there are Users with the following details:
    | uid | username | email          | password |
    | u1  | peter    | peter@test.com | testpass |
    | u2  | john     | john@test.org  | johnpass |

Databases... Ready!

It is worth pointing out that I will not be entirely dropping and subsequently recreating the entire database during each test run, but rather, just the entire contents of the database :)

Essentially we are left with a fresh, but empty shell, just waiting for us to add back in the expected table data. This makes the tests run marginally quicker - I haven't benchmarked this, but honestly, Behat test runs are not the quickest anyway so shaving a few seconds off here and there isn't currently a huge priority. This will likely change as your test suite gets bigger, but for now, it's a needless optimisation in my opinion.

If you have followed along with the installation guide, a good idea would be to add your databases to your Ansible host_vars file for your current server.

As we are using a new environment for our testing, it would be useful to add in both api and api_acceptance to our host_vars, so that any other members of the team would get the expected databases created when initially building their dev environment from our Ansible scripts.

Whatever you decide to call your database, be sure to update your app/config/parameters.yml file accordingly.

Feature Context

When you first run behat --init you should have found a new directory - features - was created for you inside the root of your project.

This folder contains a single subfolder, containing a single file - features/bootstrap/FeatureContext.php.

Now, as mentioned in earlier videos, I don't use the suggested Behat feature directory structure. I prefer to put my features inside my bundle directories. This will have an affect on the namespacing as we will see in future videos, but for now, we are going to make use of the FeatureContext.php file that Behat creates for us.

We will move this shortly. But one step at a time.

Inside this file we are going to add the following code:

<?php

// features/bootstrap/FeatureContext.php

use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;

/**
 * Defines application features from the specific context.
 */
class FeatureContext implements Context, SnippetAcceptingContext
{
    private $doctrine;
    private $manager;
    private $schemaTool;
    private $classes;

    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    public function __construct(\Doctrine\Common\Persistence\ManagerRegistry $doctrine)
    {
        $this->doctrine = $doctrine;
        $this->manager = $doctrine->getManager();

        $this->schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->manager);

        $this->classes = $this->manager->getMetadataFactory()->getAllMetadata();
    }

    /**
     * @BeforeScenario
     */
    public function createSchema()
    {
        echo '-- DROP SCHEMA -- ' . "\n\n\n";
        $this->schemaTool->dropSchema($this->classes);

        echo '-- CREATE SCHEMA -- ' . "\n\n\n";
        $this->schemaTool->createSchema($this->classes);
    }
}

Note, this is different to the code in the video. As you will see in the next video, that code caused some unanticipated problems - which will be addressed in the next video. The gist of which, by dropping the schema after a test run, we couldn't see the db state after a test run had run... again, more in the next video.

What's pretty cool about this code is that we don't need to worry about creating a list of configured entities, then loop through them, creating a table for each.

Instead, we let Doctrine worry about all of this - letting Doctrine figure out what entities are configured and then creating us a database table for each.

Feel free to remove the echo statements.

Injecting Doctrine

Note that we don't inject the EntityManagerInterface as we would tend to do inside a Symfony service.

Although to be fair, most of the time you likely should inject the required Entity Repository rather than the entity manager itself. See this excellent post from Matthias Noback for more.

Instead we inject the ManagerRepository which gives Doctrine full access to any of our configured entity managers. For most projects, multiple entity managers aren't required, but if you do have multiple entity managers, this code will see you through. And if you don't, well, all bases are covered :)

We must also update our behat.yml file to inject the ManagerRepository.

# behat.yml

default:

  suites:
    default:
      type: symfony_bundle
      bundle: AppBundle
      contexts:
        - FeatureContext:
            doctrine: "@doctrine"

There are two important things happening here.

The first is that, as explained in the previous video, we are able to use the standard Symfony way of describing our services - with @ prefix, thanks to the Symfony2Extension.

If you are unsure on Symfony services then please consider watching this short course on What Is A Symfony Service?

Secondly, whatever you call your service inside your behat.yml must match up with the variable you use inside your Feature Context file.

In this case, doctrine must match $doctrine in our constructor. An alternative:

# behat.yml
      contexts:
        - FeatureContext:
            someVarHere: "@doctrine"

Would need:

<?php

// features/bootstrap/FeatureContext.php

    public function __construct(\Doctrine\Common\Persistence\ManagerRegistry $someVarHere)
    {
    }
}

At this point we are finally ready to start writing our background steps. Crazy, eh? All this work and we haven't even started writing our test code, let alone our app code.

This is a very fair point, and worth addressing.

Setting up a project with Behat is not a quick process. If you've been following along, I can imagine you are thinking... no kidding!

But, it is worth it. With one caveat - it is worth it, if your project is expected to reach anything like a standard project level of complexity.

If you are throwing together a prototype, or learning the basics, or just practicing... this is entirely overkill.

If you are building a serious app, expecting to charge, or have customers, or use this app in production then you should seriously consider laying solid foundations. Behat, and PHPSpec as we will see, are fundamental parts of the process.

Not only will this level of testing help you during development in both catching regressions / bugs, AND improving the design of your code, but also - and more often importantly - it will give you the confidence to change.

If anything is sure in software it is that requirements will change.

During development you are actively changing stuff all the time. Change is fine. Make a change, see what breaks, fix it and carry on.

Some time later, the project reaches a level of maturity - and ultimately, goes into production.

Time goes by, and 6 weeks or 6 months later, some stakeholder (your boss, customer, you?) decides that actually, feature X should also do A, B, and maybe C?

Well, you no longer have that day to day intricate knowledge of your code. Now changing things becomes harder. Without tests, it becomes a nightmare.

Thankfully though, you put in the groundwork and laid a solid foundation. Sure, you still feel a little rusty in your 'old' code, but the tests reliably prove you didn't accidentally torpedo some unexpected piece of functionality. If you work in an enterprise, this can be the difference between keeping and losing your job.

Of course, it's up to you. It does take time. But Rome wasn't built in a day :)

Code For This Course

Get the code for this course.

Episodes