Test Fixtures & Fast Databases!

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 $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

In practice, the hardest thing about functional testing isn't all the stuff about clicking links or filling out forms. Nope, the toughest thing is taking control of the database!

To Fixture or Not to Fixture?

To do this, there are two big philosophies! First, like with integration tests, we can decide to always start the database empty. And then, if we need data - like we need to add some Enclosures to the database - we will add that data inside the test method itself. This is how I normally code. It's not super fancy, and it means that you need to do extra work in each test to create the exact data you need. But, it also means that each test reads like a complete story. For example, at the top of this test, you would be able to see that we created 3 Enclosure objects. Then, at the bottom, it will make sense why we're expecting to see 3 rows in the table.

The second philosophy, which is a bit simpler, is to load data fixtures. This is what we're going to do: but I'll mention how things would be different if you want to use the first philosophy.

Adding Data Fixtures

First, install the DoctrineFixturesBundle:

composer require --dev doctrine/doctrine-fixtures-bundle:2.4.1

If you downloaded the course code, in the tutorial/ directory, you should have a DataFixtures directory. Copy that into your AppBundle.

... lines 1 - 10
class LoadBasicParkData extends AbstractFixture implements OrderedFixtureInterface
{
public function load(ObjectManager $manager)
{
$carnivorousEnclosure = new Enclosure();
$manager->persist($carnivorousEnclosure);
$this->addReference('carnivorous-enclosure', $carnivorousEnclosure);
$herbivorousEnclosure = new Enclosure();
$manager->persist($herbivorousEnclosure);
$this->addReference('herbivorous-enclosure', $herbivorousEnclosure);
$manager->persist(new Enclosure(true));
$this->addDinosaur($manager, $carnivorousEnclosure, 'Velociraptor', true, 3);
$this->addDinosaur($manager, $carnivorousEnclosure, 'Velociraptor', true, 1);
$this->addDinosaur($manager, $carnivorousEnclosure, 'Velociraptor', true, 5);
$this->addDinosaur($manager, $herbivorousEnclosure, 'Triceratops', false, 7);
$manager->flush();
}
... lines 33 - 52
}

... lines 1 - 10
class LoadSecurityData extends AbstractFixture implements OrderedFixtureInterface
{
public function load(ObjectManager $manager)
{
$herbivorousEnclosure = $this->getReference('herbivorous-enclosure');
$this->addSecurity($herbivorousEnclosure, 'Fence', true);
$carnivorousEnclosure = $this->getReference('carnivorous-enclosure');
$this->addSecurity($carnivorousEnclosure, 'Electric fence', false);
$this->addSecurity($carnivorousEnclosure, 'Guard tower', false);
$manager->flush();
}
... lines 26 - 39
}

These two classes build 3 Enclosures and also add some security to them. But, part of this code is using a setEnclosure() method on Dinosaur... and that doesn't exist! Open Dinosaur, scroll to the bottom, and add it: public function setEnclosure() with an Enclosure argument. Set that on the property.

... lines 1 - 10
class Dinosaur
{
... lines 13 - 81
public function setEnclosure(Enclosure $enclosure)
{
$this->enclosure = $enclosure;
}
}

Awesome! Once the bundle finishes downloading open AppKernel. And inside the if statement, add new DoctrineFixturesBundle(). If you're using Flex, this step will have already been done for you automatically.

... lines 1 - 7
class AppKernel extends Kernel
{
public function registerBundles()
{
... lines 12 - 22
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
... lines 24 - 26
$bundles[] = new DoctrineFixturesBundle();
... lines 28 - 36
}
... lines 38 - 39
}
... lines 41 - 60
}

We haven't hooked the fixtures into our tests yet, but we can at least try them! Run:

php bin/console doctrine:fixtures:load

Go check out the browser! Yes! The fixtures gave us 3 enclosures. That's why I wrote our test to expect 3 rows. If we can load the fixtures when the test runs, we're in business!

Loading Fixtures in the Test

Fortunately, LiipFunctionalTestBundle gives us a really nice way to do this. At the top of the test method, add $this->loadFixtures() and pass an array of the fixture classes you want to load: LoadBasicParkData::class and LoadSecurityData::class.

... lines 1 - 8
class DefaultControllerTest extends WebTestCase
{
public function testEnclosuresAreShownOnHomepage()
{
$this->loadFixtures([
LoadBasicParkData::class,
LoadSecurityData::class,
]);
... lines 17 - 25
}
}

Tip

Since LiipFunctionalTestBundle v3.0 the loadFixtures() method is no longer supported. You should use LiipTestFixturesBundle instead

If you're going to use the same set of fixtures for all your test methods, then moving this to setUp() is a great choice.

Run the tests!

./vendor/bin/phpunit tests/AppBundle/Controller/DefaultControllerTest.php

They work! They pass, over and over again!

So, how would things be different if you did not want to load fixtures? Well, you will still want to empty the database. So, you could use the same trick as the integration tests. Or, you could call $this->loadFixtures() with an empty array.

Of course, the tests fail. That's because loadFixtures() empties the database... but then doesn't load anything into it.

Remember, if you choose this philosophy, you're now responsible for creating the data you need. How? The same way you always do: create some Enclosure objects and then persist them with the EntityManager. And since we're still ultimately extending KernelTestCase, we already know how to get the EntityManager: with self::$kernel->getContainer()->get('doctrine')->getManager().

Test Base Classes

Actually, it would be great if we had a shortcut like $this->getEntityManager() for all our test classes. We won't do it in this tutorial, but I highly recommend creating your own base test class with extra shortcut methods. Typically, I'll have one base test class for integration tests - which extends KernelTestCase - and if necessary, another for my functional tests, which extends WebTestCase. You can also use traits to share code even better.

Faster Database Loading

The LiipFunctionalTestBundle has two other tricks. First, if you're using SQLite, then it automatically builds the schema for you. Check this out: delete the database file:

rm var/data/test.sqlite

Bye bye database schema! But, when you run the tests, they still pass! When you load the fixtures, it creates the schema too. Thanks friends!

The second trick lives in app/config/config_test.yml. Add a new option: cache_sqlite_db set to true.

... lines 1 - 27
liip_functional_test:
cache_sqlite_db: true

Visually... this doesn't make any difference. BUT! Behind the scenes, cool things are happening. Each time you call loadFixtures(), it loads the fixtures and then caches the database file. The next time you call loadFixtures() with the same arguments, it instantly re-uses that cached database file.

Check this out: to simulate loading a lot of fixtures, add a sleep(5) in one of them. Now, run the test:

./vendor/bin/phpunit tests/AppBundle/Controller/DefaultControllerTest.php

Yea... it's slow. The bundle detected the change we made and was smart enough to know that it needed to reload the fixtures. But the second time... zoom! It's super fast.

The coolest part is that all of this database and fixture-handling stuff from LiipFunctionalTestBundle can be used even if you decide to use a different client - like Mink - instead of Symfony's BrowserKit.

Next, let's look at one more trick you can do with fixtures.

Leave a comment!

  • 2019-07-31 Thomas Vangelooven

    My pleasure, just helping people get over the same WTF moment I had :)

  • 2019-07-31 Victor Bocharsky

    Hey Thomas,

    Thank you for sharing this with others!

    Cheers!

  • 2019-07-30 Thomas Vangelooven

    Starting from LiipFunctionTestBundle ^3.0.0 loadFixtures is no longer support.
    From de Changelog:

    Removed fixtures loading in favor of https://github.com/liip/Lii...
  • 2019-06-05 Victor Bocharsky

    Hey Kuba,

    That was totally a bug, not sure how we missed it when was recording the course. Anyway, it's fixed in the source code and code blocks now. And yes, you're right, we need to extend "Doctrine\Common\DataFixtures\AbstractFixture" instead - good guess! So, it's fixed and I just double checked - fixtures are loaded fine now. Literally, changes were: https://github.com/knpunive...

    Thank you for reporting this!

    Cheers!

  • 2019-05-28 Diego Aguiar

    Hey Kuba Florczuk

    Wow, you are totally right. I was on a latest release of DoctrineFixturesBundle where Fixture class doesn't implement DependentFixtureInterface anymore. I think your solution is good enough, just extend from Doctrine\Common\DataFixtures\AbstractFixture

    We will see what we can do about tutorial's code

    Cheers!

  • 2019-05-28 Kuba Florczuk

    Diego Aguiar LoadBasicParkData extends Doctrine\Bundle\FixturesBundle\Fixture which implements DependentFixtureInterface...
    When we remove extended class - addReference method is no longer available.
    When we remove implementation of OrderedFixtureInterface, well it won't be ordered.
    So it's clearly bug in course code.

    Probably you should extend Doctrine\Common\DataFixtures\AbstractFixture.

  • 2019-05-23 Diego Aguiar

    Hey Stéphane, actually this course is based on Symfony3.3 so it should just work but I just checked the code and we don't implement both interfaces on fixtures classes, somehow you added it. Just stop implementing DependentFixtureInterface and it should work

    Cheers!

  • 2019-05-23 Stéphane

    Hey Diego,
    Thank for your reply. But I don't understand why it's not working because I only copy the files about fixtures from tutorial folder. Normaly the code can work with Sf3 ?

  • 2019-05-22 Diego Aguiar

    Hey Stéphane

    Yep, you can't implement both interfaces :p
    If you are on Symfony3 I believe you should only implement "OrderedFixtureInterface"

    Cheers!

  • 2019-05-22 Stéphane

    Hey,

    When I load fixtures : php bin/console doctrine:fixtures:load, I have an error message :
    [InvalidArgumentException]
    Class "AppBundle\DataFixtures\ORM\LoadBasicParkData" can't implement "OrderedFixtureInterface" and "DependentFixtureInterface" at the same time.

    I use Symfony 3.3.18 and doctrine-fixtures-bundle": "2.4.1"

    You know why ?

  • 2019-03-21 Vladimir Sadicov

    Hey Bojan Đurković

    Thanks for sharing information about bundle compatibility.

    BTW to follow course code it will be better to use fixtures-bundle version 2.4.1.

    Cheers!

  • 2019-03-21 Bojan Đurković

    Hi!

    As of this moment, Symfony 3.3.10 is not supported any more by the latest version of doctrine/doctrine-fixtures-bundle. The version 3.0.4 does support Symfony 3.3.10. So to get this to work, `composer require --dev doctrine/doctrine-fixtures-bundle:3.0.4` should be the full command.

  • 2018-09-17 Victor Bocharsky

    Hey Nicolas!

    Thanks for reporting it! Now it's fixed :)

    Cheers!

  • 2018-09-16 Nicolás González Flores

    Hi guys,

    there is a typo in the video filename when you download it.

    Cheers