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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIn 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.
Hi,
when I try run this test "./vendor/bin/phpunit tests/AppBundle/Controller/DefaultControllerTest.php", it fails with the following error: "BadMethodCallException: doctrine/doctrine-fixtures-bundle must be installed to use this method."
In composer.json I have these versions:
"doctrine/doctrine-fixtures-bundle": "2.3",
"liip/functional-test-bundle": "~2.0@alpha",
"liip/test-fixtures-bundle": "^1.0.0",
"phpunit/phpunit": "^8.5",
"doctrine/data-fixtures": "1.3",
"doctrine/doctrine-bundle": "^1.6".
And In AppKernel.php I've registered these bundles for 'dev' and 'test' environments:
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new DoctrineFixturesBundle();
if ('dev' === $this->getEnvironment()) {
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
$bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle();
}
if ('test' === $this->getEnvironment()) {
$bundles[] = new LiipFunctionalTestBundle();
}
I tried to google for the answer, but didn't find helpful info. Could you help me out to figure out why i get this error, please?