Resetting the Database Between Tests

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

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

Login Subscribe

One of the trickiest things about functional tests is controlling the database. In my opinion, a perfect world is one where the database is completely empty at the start of each test. That would give us a completely predictable state where we could create whatever User or CheeseListing objects we want and know exactly what's in the database.

Some people prefer to go a step further and load a predictable set of "fixtures" data before each test - like a few users and some cheese listings. That's fine, but it's not the approach I prefer. Why? Because if we can make the database empty, then each test is forced to create whatever data - like users or cheese listings - that it needs. That might sound bad at first... because... that's more work! But the end result is that each test reads like a complete story: we can see that we get a 401 status code, then we create a user, then we log in with the user we just created. A nice, complete story.

Installing Alice

So... how can we empty the database before each test? There are a few answers, but one common one you'll see in the API Platform world is called Alice. Find your terminal and install it with:

composer require alice --dev

This will install hautelook/alice-bundle. What does that bundle actually do? I've talked about it a few times in the past on SymfonyCasts: it allows you to specify fixtures data via YAML. It's really fun actually and has some nice features for quickly creating a set of objects, using random data and linking objects to each other. It was the inspiration behind a fixture class that we created and used in our Symfony Doctrine tutorial. The recipe creates a fixtures/ directory for the YAML files and a new command for loading that data.

The ReloadDatabaseTrait

But... what does any of that have to do with testing? Nothing! It's just a way to load fixture data and you can use it... or not use it. But AliceBundle has an extra, unrelated, feature that helps manage your database in the test environment.

Back in our test class... once PHPStorm finishes reindexing we're going to use a new trait: use ReloadDatabaseTrait.

... lines 1 - 6
use Hautelook\AliceBundle\PhpUnit\ReloadDatabaseTrait;
... line 8
class CheeseListingResourceTest extends ApiTestCase
{
use ReloadDatabaseTrait;
... lines 12 - 39
}

That's it! Just by having this, before each test method is called, the trait will handle emptying our database tables and reloading our Alice YAML fixtures... which of course we don't have. So, it'll just empty the database.

Try it!

php bin/phpunit

We see even more deprecation warnings now - the AliceBundle has a few deprecations it needs to take care of - but it works! We can run it over and over again... it always passes because the database always starts empty. This is a huge step towards having dependable, readable tests.

Removing the Logging Output

While we're here, we're getting this odd log output at the top of our tests. I can tell that this is coming from Symfony... it's almost like each time an "error" log is triggered in our code, it's being printed here. That's... ok... but why? Symfony normally stores log files in var/logs/test.log, for the test environment.

The answer is... because we never installed a logger! Internally, Symfony ships with its own logger service so that if any other services or bundles want to log something, it's available! But that logger is super simple... on purpose: it's just meant as a fallback logger if nothing better is installed. Instead of writing to a file, it logs errors to stderr... which basically means they get printed to the screen from the command line.

Let's install a real logger:

composer require logger

This installs Monolog. When it finishes... try running the tests again:

php bin/phpunit

And... no more output! Now the logs are stored in var/logs/test.log. If you tail that file...

tail var/logs/test.log

there it is!

Next, I want to make one more improvement to our test suite before we get back to talking about API Platform security. I want to create a base test class with some helper methods that will enable us to move fast and write clean code in our tests.

Leave a comment!

  • 2020-06-29 Diego Aguiar

    I would blame IDE auto-completion if I were you ;)

  • 2020-06-29 David Rojo

    Why on earth I've used RefreshDatabaseTrait instead of ReloadDatabaseTrait? these are some dev misteries that we will never know ;)

    I've changed RefreshDatabaseTrait to ReloadDatabaseTrait and everything works without needing the $client->disableReboot()

    I don't know how I ended using RefreshDatabaseTrait, but thanks to it, we know something new today. Sorry for the inconvenience.

  • 2020-06-29 weaverryan

    Yo David Rojo!

    Good find with the kernel shutdown - I was thinking the exact same thing. To be honest, we use ReloadDatabaseTrait in this tutorial, which works slightly differently - it loads fixtures on boot, but does nothing on shutdown. It's totally possible - as crazy as it sounds - that RefreshDatabaseTrait isn't working for *anyone* correctly. Tbh, I like the ReloadDatabaseTrait because it works well for us, but I (in general) am a bit skeptic of the Alice library - I don't always like the way it works. From what I can see (and as you experienced), RefreshDatabaseTrait ::ensureKernelShutdown will be called before every request (except for the first)... which means your data would be wiped out. I would steer clear of this trait :/.

    If you want similar functionality, try out https://github.com/dmaicher... - all of this stuff contains some magic, but that's a solid bundle.

    Cheers!

  • 2020-06-26 David Rojo

    I've been dealing with this all the day but I haven't found anything in any repo.

  • 2020-06-26 Diego Aguiar

    Hey David Rojo

    I'm glad to hear that you could fix your problem. I also think that's a weird behavior. Have you checked if there is an issue on the libraries repository?

  • 2020-06-26 David Rojo

    I've found the solution. Each request forces the kernel to shutdown (https://github.com/symfony/..., this includes wiping the database when using RefreshDatabaseTrait.

    I added after creating the client the following line:

    $client = self::createClient();
    $client->disableReboot();

    And now it works, but I think that the database shouldn't be deleting on each request of the client...

  • 2020-06-26 David Rojo

    Hi, I am having a strange behaviour with RefreshDatabaseTrait.

    My entities seems to be removed after I am accessing them when using phpunit, this is my test code having added the use RefreshDatabaseTrait in the class:

    $artist = $this->createArtist('Test artist');

    $client->request('GET', '/api/artists/'.$artist->getId());
    $this->assertResponseIsSuccessful();

    $client->request('GET', '/api/artists/'.$artist->getId());
    $this->assertResponseIsSuccessful();

    As you can see "nothing changes" in the two requests, byt the first assert is successfull and the second assert fails and I receive the following error:

    1) App\Tests\Functional\Api\ArtistFollowersTest::testFollowArtist
    Failed asserting that the Response is successful.
    HTTP/1.1 404 Not Found

    If I remove the use RefreshDatabaseTrait then the test goes ok. It is like after the request the database is erased.

    Also I can't login, as after each request the user is removed and I can't call any api operation that requires the user as the user no longer exists :S

  • 2020-04-27 Vladimir Sadicov

    Hey Julien Bonnier

    We are sorry that you got this issue, Thanks for sharing you solution! BTW we updated base code of Api platform tutorial so it should be issue free now )

    Cheers!

  • 2020-04-25 Julien Bonnier

    For some reason I ran into an error with requirements unmet while trying to install alice.

    Your requirements could not be resolved to an installable set of packages.

    Problem 1
    - Conclusion: don't install hautelook/alice-bundle 2.7.2
    - Conclusion: don't install hautelook/alice-bundle v2.7.1
    - Conclusion: remove doctrine/persistence 1.1.1
    - Installation request for hautelook/alice-bundle ^2.7 -> satisfiable by hautelook/alice-bundle[2.7.2, v2.7.0, v2.7.1].
    - Conclusion: don't install doctrine/persistence 1.1.1
    - hautelook/alice-bundle v2.7.0 requires doctrine/persistence ^1.3.4 -> satisfiable by doctrine/persistence[1.3.4, 1.3.5, 1.3.6, 1.3.7].
    - Can only install one of: doctrine/persistence[1.3.4, 1.1.1].
    - Can only install one of: doctrine/persistence[1.3.5, 1.1.1].
    - Can only install one of: doctrine/persistence[1.3.6, 1.1.1].
    - Can only install one of: doctrine/persistence[1.3.7, 1.1.1].
    - Installation request for doctrine/persistence (locked at 1.1.1) -> satisfiable by doctrine/persistence[1.1.1].


    Installation failed, reverting ./composer.json to its original content.

    Simply running composer update doctrine/persistence and then composer require alice --dev fixed the issue.

  • 2020-01-10 Diego Aguiar

    Woooh, nice! Didn't know about that bundle. It seems much faster than my approach. Thanks for sharing it!

  • 2020-01-10 Gabb

    actually what I did was install the Doctrine Test Bundle and it also refreshes the db after every test. Found here https://symfony.com/doc/cur...

  • 2020-01-09 Diego Aguiar

    Hey Gabb

    As Ramazan said, they just add support for Sf5
    But, if you are still interested in how to clear your DB "manually", you can do one of these examples


    // delete one table
    $alias = SomeEntity::class;
    $entityManager
    ->createQuery(sprintf('DELETE FROM %s', $alias))
    ->execute()
    ;



    // delete all tables
    use Doctrine\Common\DataFixtures\Purger\ORMPurger;

    $purger = new ORMPurger($entityManager);
    $purger->purge();

    Cheers!

  • 2020-01-09 Ramazan

    Enable support for Symfony 5 #487
    https://github.com/hauteloo...

  • 2020-01-09 Gabb

    Is there a different way to reload the db? AliceBundle does not support symfony 5.

  • 2019-09-04 Victor Bocharsky

    Hey Pawel,

    Yes, it should be possible. If you look at BaseDatabaseTrait that is used by ReloadDatabaseTrait - it has "static::$manager" call, that means you should be able to create "protected static $manager" properly and set it to *your* manager that you want to be used.

    Otherwise, consider copy/pasting the code from those trait and tweak it to your needs - they are just shortcuts for having this functional.

    Cheers!

  • 2019-09-03 Paweł Banaś

    Is there any way to override $manager used by this trait? we're using more than one manager and for the default one there's no access to schema operations.

  • 2019-09-02 Ramazan

    thank you Paweł Banaś , this helped.

    Now it's clear.

  • 2019-08-30 Paweł Banaś

    Try adding this in phpunit xml configuration

    <env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/>

  • 2019-08-28 Diego Aguiar

    Hey Ramazan

    I'm not sure why or what Ryan did to avoid those deprecations to show up but if you are not interested on seeing them you can configure PHPUnit to do so. Give it a check to the documentation: https://symfony.com/doc/cur...

    I hope this helps. Cheers!

  • 2019-08-28 Ramazan

    Hello,

    I wonder why do you get the deprecation message only once while I have them every time I run the tests?