Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add traits to purge the DB and load fixtures in PHPUnit tests #432

Merged
merged 7 commits into from Sep 24, 2018

Conversation

@dunglas
Copy link
Contributor

commented Sep 19, 2018

Edit: the annotation cannot work with the current design of KernelTestCase, I switched to a trait that has the other advantage of being super close of what Laravel does.

The bundle provides nice helpers, inspired by Laravel, dedicated for database testing: RefreshDatabaseTrait and ReloadDatabaseTrait.
These traits allow to easily reset the database in a known state before each PHPUnit test: it purges the database then loads the fixtures.

They are particularly helpful when writing functional tests and when using Panther.

To improve performance, RefreshDatabaseTrait populates the database only one time, then wraps every tests in a transaction that will be rolled back at the end after its execution (regardless of if it's a success or a failure):

<?php

namespace App\Tests;

use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class NewsTest extends WebTestCase
{
    use RefreshDatabaseTrait;

    public function postCommentTest()
    {
        $client = static::createClient(); // The transaction starts just after the boot of the Symfony kernel
        $crawler = $client->request('GET', '/my-news');
        $form = $crawler->filter('#post-comment')->form(['new-comment' => 'Symfony is so cool!']);
        $client->submit($form);
        // At the end of this test, the transaction will be rolled back (even if the test fails)
    }
}

Sometimes, wrapping tests in transactions is not possible. For instance, when using Panther, changes to the database are made by another PHP process, so it wont work.
In such cases, use the ReloadDatabase trait. It will purge the DB and load fixtures before every tests:

<?php

namespace App\Tests;

use Hautelook\AliceBundle\PhpUnit\ReloadDatabaseTrait;
use Symfony\Component\Panther\PantherTestCase;

class NewsTest extends PantherTestCase // Be sure to extends KernelTestCase, WebTestCase or PantherTestCase
{
    use ReloadDatabaseTrait;

    public function postCommentTest()
    {
        $client = static::createPantherClient();// The database will be reset after every boot of the Symfony kernel

        $crawler = $client->request('GET', '/my-news');
        $form = $crawler->filter('#post-comment')->form(['new-comment' => 'Symfony is so cool!']);
        $client->submit($form);
    }
}

This strategy doesn't work when using Panther, because the changes to the database are done by another process, outside
of the transaction.

Both traits provide several configuration options as protected static properties:

  • self::$manager: The name of the Doctrine manager to use
  • self::$bundles: The list of bundles where to look for fixtures
  • self::$append: Append fixtures instead of purging
  • self::$purgeWithTruncate: Use TRUNCATE to purge
  • self::$shard: The name of the Doctrine shard to use
  • self::$connection: The name of the Doctrine connection to use

Use them in the setUpBeforeClass method.

<?php

namespace App\Tests;

use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class NewsTest extends WebTestCase
{
    use RefreshDatabaseTrait;

    public static function setUpBeforeClass()
    {
        self::$append = true;
    }

    // ...
}

Old comment (previous commits):


This PR introduces a new PHPUnit listener purge the DB and load the fixtures before every test.
It can work by just purging, or to purge only one time then wrap tests in transactions that will be rollbacked at the end (faster, but not 100% reliable, and don't work for E2E tests like Panther).

Like the Symfony PHPUnit Bridge, it relies on special PHPUnit groups:

  • @group refresh-database: purge and load the fixtures
  • @group refresh-database-transaction: same and use transactions

This feature is heavily inspirited from Laravel database testing traits (from a DX point of view, obviously internals cannot be similar).

Example usage with Panther:

<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <!-- ... -->
    <listeners>
        <!-- ... -->
        <listener class="Hautelook\AliceBundle\PhpUnit\RefreshDatabaseListener" />
    </listeners>
</phpunit>
App\Entity\Comment:
    comment_{1..10}:
        news: <randomElement(["week-601", "symfony-live-usa-2018"])>
        body: <paragraph()>
<?php

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;

/**
 * @group refresh-database
 */
class CommentsTest extends PantherTestCase
{
    public function testComments()
    {
        $client = static::createPantherClient();
        $crawler = $client->request('GET', '/news/symfony-live-usa-2018');

        $client->waitFor('#post-comment'); // Wait for the form to appear, it may take some time because it's done in JS
        $form = $crawler->filter('#post-comment')->form(['new-comment' => 'Symfony is so cool!']);
        $client->submit($form);

        $client->waitFor('#comments li:nth-child(6)'); // Wait for the comment to appear
        $this->assertSame('Symfony is so cool!', $crawler->filter('#comments li:last-child')->text());
    }

    public function testRefreshDatabase()
    {
        $client = static::createPantherClient();
        $crawler = $client->request('GET', '/news/symfony-live-usa-2018');

        $client->waitFor('#comments'); // Wait for the comments to appear

        // Be sure that the database has been reseted
        $this->assertNotSame('Symfony is so cool!', $crawler->filter('#comments li:last-child')->text());
    }
}

TODO:

  • Add tests
Copy link
Collaborator

left a comment

LGTM for the feature 👍

$kernel = $this->getKernel($test);
$container = $kernel->getContainer();
$doctrine = $container->get('doctrine');
$transaction = \in_array('transaction', $groups, true);

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 20, 2018

Collaborator

let's import the functions rather than adding a leading backslash, since unlike Symfony we don't have to support PHP 5.x, this will not be an issue. Same for constants and classes belonging to the global scope (although I know it's not consistent everywhere for the 3 alice related project code-bases)

This comment has been minimized.

Copy link
@dunglas

dunglas Sep 20, 2018

Author Contributor

We should update the PHP CS Fixer config then

This comment has been minimized.

Copy link
@dunglas

dunglas Sep 21, 2018

Author Contributor

Actually, PHP CS Fixer automatically removes functions' import to optimize it...

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 21, 2018

Collaborator

But IIRC that's a config entry. I would love a PHP-CS-Fixer rule to do the import function statements but that's not the case for now. So in the meantime I would just disable that cofnig for PHP-CS-Fixer, Hautelook doesn't really have critical performance code anyway, if optims can be done it would rather be at Alice & the ORM level

}
$container->get('hautelook_alice.loader')->load(
new Application($kernel), // OK this is ugly... But there is no other way without redesigning LoaderInterface from the ground.

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 20, 2018

Collaborator

that's fine for now... maybe a TODO could be added to the load() method to suggest changing the typehint for HautelookAliceBundle 3.x

use TestListenerDefaultImplementation;
// These public properties provide a way to globally configure the listener during bootstrap
public static $manager = null;

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 20, 2018

Collaborator

While I love with the "omit the types when obvious" policy to reduce the noise, the types here are not obvious so I would add them for this case

self::$connections[$test]->beginTransaction();
}
public function endTest(Test $test, $time): void

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 20, 2018

Collaborator

can be KernelTestCase here. If you cannot change the signature because of the parent class, I would add an inheritdoc block to indicate you are implementing/overriding the parent method + add @param. Same for the others

This comment has been minimized.

Copy link
@dunglas

dunglas Sep 20, 2018

Author Contributor

Not on this one because the listener will call it for every tests. I’ll update the private methods.

@dunglas dunglas force-pushed the dunglas:phpunit-listener branch from 58980d4 to 5dadfc2 Sep 21, 2018
dunglas added 2 commits Sep 19, 2018
@dunglas dunglas force-pushed the dunglas:phpunit-listener branch from aa72ba4 to e358a0f Sep 22, 2018
@dunglas dunglas force-pushed the dunglas:phpunit-listener branch from e358a0f to 74b0c70 Sep 22, 2018
README.md Outdated
public static function setUpBeforeClass()
{
self::$useTransactions = true; // Enable the transactional mode

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 23, 2018

Collaborator

shouldn't that mode be the default one?

This comment has been minimized.

Copy link
@dunglas

dunglas Sep 23, 2018

Author Contributor

It's not 100% safe, but I'll try something else.

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 23, 2018

Collaborator

When is it not?

This comment has been minimized.

Copy link
@dunglas

dunglas Sep 23, 2018

Author Contributor

@theofidry when the DB is modified by another process (like when using Panther) for instance.

README.md Outdated
public function aTest()
{
$client = static::createPantherClient();
// The transaction starts here

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 23, 2018

Collaborator

how come it doesn't start earlier? I thought it would start as soon as you inter in the test

This comment has been minimized.

Copy link
@dunglas

dunglas Sep 23, 2018

Author Contributor

It starts when the kernel boot. Here it's createPantherClient that boots the kernel. It's intended I think. I'll reword to make this statement bolder.

README.md Outdated
$crawler = $client->request('GET', '/my-news');
$form = $crawler->filter('#post-comment')->form(['new-comment' => 'Symfony is so cool!']);
$client->submit($form);
// At the end of this test, the transaction will be rolled back

This comment has been minimized.

Copy link
@theofidry

theofidry Sep 23, 2018

Collaborator

Might be worth mentioning that it will be the case even though the test will fail

@theofidry

This comment has been minimized.

Copy link
Collaborator

commented Sep 23, 2018

Looks like there is a test failing possible due to using a too old version of Symfony but we can bump the requirement if necessary

@dunglas dunglas changed the title Add a PHPUnit listener to load fixtures Add traits to purge the DB and load fixtures in PHPUnit tests Sep 23, 2018
@dunglas dunglas force-pushed the dunglas:phpunit-listener branch from 61991a8 to ad28426 Sep 23, 2018
@dunglas

This comment has been minimized.

Copy link
Contributor Author

commented Sep 23, 2018

PR ready for another review!
Now there is 2 traits, one using transactions (RefreshDatabase), and another doing a purge before every tests for Panther (ReloadDatabase).

I also updated the docs.

@dunglas dunglas force-pushed the dunglas:phpunit-listener branch from ad28426 to ae8676a Sep 23, 2018
@dunglas dunglas force-pushed the dunglas:phpunit-listener branch from ae8676a to cf58032 Sep 23, 2018
Copy link
Contributor

left a comment

:)

README.md Outdated
@@ -152,6 +156,103 @@ If you want to load the fixtures of a bundle only, do `php bin/console hautelook
Next chapter: [Advanced usage](doc/advanced-usage.md)


## Database testing

The bundle provides nice helpers, [inspirited by Laravel](https://laravel.com/docs/5.6/database-testing#resetting-the-database-after-each-test),

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

inspired by Laravel

README.md Outdated
## Database testing

The bundle provides nice helpers, [inspirited by Laravel](https://laravel.com/docs/5.6/database-testing#resetting-the-database-after-each-test),
dedicated for database testing: `RefreshDatabaseTrait` and `ReloadDatabaseTrait`.

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

for testing with a database:

README.md Outdated

The bundle provides nice helpers, [inspirited by Laravel](https://laravel.com/docs/5.6/database-testing#resetting-the-database-after-each-test),
dedicated for database testing: `RefreshDatabaseTrait` and `ReloadDatabaseTrait`.
These traits allow to easily reset the database in a known state before each PHPUnit test: it purges the database then loads

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

These traits allow you to easily reset the database to a known state before each PHPUnit test: it purges the database then loads

README.md Outdated
These traits allow to easily reset the database in a known state before each PHPUnit test: it purges the database then loads
the fixtures.

They are particularly helpful when writing [functional test](https://symfony.com/doc/current/testing.html#functional-tests)

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

functional tests

README.md Outdated
They are particularly helpful when writing [functional test](https://symfony.com/doc/current/testing.html#functional-tests)
and when using [Panther](https://github.com/symfony/panther).

To improve performances, `RefreshTrait` populates the database only one time, then wraps every tests in a

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

performances

RefreshDatabaseTrait

README.md Outdated
class NewsTest extends PantherTestCase // Be sure to extends KernelTestCase, WebTestCase or PantherTestCase
{
use RefreshDatabaseTrait;

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

Should be Reload

README.md Outdated
public function postCommentTest()
{
$client = static::createPantherClient(); // The database will be reseted after every boots of the Symfony kernel

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

// The database will be reset after every boot of the Symfony kernel

README.md Outdated
}
```

This strategy doesn't work when using Panther, because the changes to the databases are done by another process, outside

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

databases

/**
* @var string|null The name of the Doctrine manager to use
*/
public static $manager;

This comment has been minimized.

Copy link
@weaverryan

weaverryan Sep 23, 2018

Contributor

Do these need to be public?

This comment has been minimized.

Copy link
@dunglas

dunglas Sep 23, 2018

Author Contributor

Not anymore, it was a "rest" of the previous implementation. Updated, thanks.

@dunglas

This comment has been minimized.

Copy link
Contributor Author

commented Sep 23, 2018

Thanks for proof-reading @weaverryan! Updated.

dunglas added 2 commits Sep 23, 2018
@theofidry

This comment has been minimized.

Copy link
Collaborator

commented Sep 24, 2018

All good for me, thanks!

@theofidry theofidry merged commit 1cbbbf6 into hautelook:master Sep 24, 2018
1 check passed
1 check passed
continuous-integration/travis-ci/pr The Travis CI build passed
Details
@dunglas dunglas deleted the dunglas:phpunit-listener branch Sep 24, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.