Speed up your Laravel Dusk tests

in panel, php, laravel

Here at Pterodactyl we use Laravel Dusk to perform automated browser testing of the application in development. Dusk provides a fluent interface for interacting with Facebook's PHP Webdriver implementation, and allows us to quickly generate tests.

Dusk also ships with the ability to hot-swap environments allowing us to run tests against a testing database rather than obliterating the development database (not that it hasn't happened before). This is done by adding the DatabaseMigrations trait to the base Dusk class. When this is done each test that is booted will run the migrate:fresh artisan command thus providing it with a fresh database. While this is immensely useful (and required, since we cannot use database transactions in the browser tests), it does cause our tests to run slower due to the amount of migrations that need to be run.

Out of the box I could not find anything that would allow me to only run the migrations once per test class. Many articles online discuss only running the migrations once at the beginning of the entire test suite, but I still wanted a fresh database for each class. To implement this behavior I removed the default DatabaseMigrations trait, and instead leveraged PHPUnit's setUpBeforeClass function which runs once per class.

Initially, this was a little tricky since this function is static, and runs so early in the process that the container and application have not been booted. However, we can simply require the application bootstrap, and then mnaually call the migrate:fresh command using the kernel instance.

/**
 * Create a fresh database instance before each test class is initialized. This is different
 * than the default DatabaseMigrations as it is only run when the class is setup. The trait
 * provided by Laravel will run on EACH test function, slowing things down significantly.
 *
 * If you need to reset the DB between function runs just include the trait in that specific
 * test. In most cases you probably wont need to do this, or can modify the test slightly to
 * avoid the need to do so.
 */
public static function setUpBeforeClass()
{
    parent::setUpBeforeClass();

    $app = require __DIR__ . '/../../bootstrap/app.php';

    /** @var \Pterodactyl\Console\Kernel $kernel */
    $kernel = $app->make(\Pterodactyl\Console\Kernel::class);

    $kernel->bootstrap();
    $kernel->call('migrate:fresh');
}

As the docblock explains, this will run the migrations once per class, rather than before each test in the class. I did have to make a few quick adjustments to a test as it was creating a new user on each run (as intended), but was not using faker to generate the users email, thus leading to data integrity exceptions as the unique column rule was being violated. Check out the commit to see exactly what was added and changed to make this work.

Results

This simple change sped up our (admitedly tiny) suite of tests by close to 50%. Obviously the suite is quite small, but depending on how many tests you perform per-class you could see more or less improvement.

Before

root@app:~/app# php artisan dusk
PHPUnit 7.2.6 by Sebastian Bergmann and contributors.

.............                                                     13 / 13 (100%)

Time: 1.16 minutes, Memory: 18.00MB

OK (13 tests, 61 assertions)

After

root@app:~/app# php artisan dusk
PHPUnit 7.2.6 by Sebastian Bergmann and contributors.

.............                                                     13 / 13 (100%)

Time: 37.96 seconds, Memory: 22.00MB

OK (13 tests, 61 assertions)

Comments