Working with Laravel Dusk

Mike Smith
6 min readJan 24, 2017

--

An exciting (for me anyway) new piece of functionality coming in Laravel 5.4 is Laravel Dusk, providing an expressive, easy-to-use browser automation and testing API. I’ve been working on a new application recently, and was holding off on introducing browser tests until 5.4 got released. But the new application is purely for my own edification, so why not dive in early?

It’s worth noting that I am running Dusk locally on OSX.

Adding Dusk to your project

Unfortunately, you can’t just pull in Dusk to older versions of Laravel. Due to various dependencies, Dusk is not compatible with Laravel 5.3, so you need to update to the 5.4 framework. I would recommend reviewing the upgrade guide to see if there’s anything there that might cause you trouble.

composer require laravel/framework:5.4.*

If you already have some tests in place, you’re going to need to do a bit of fiddling around to get those old tests to work. Taylor Otwell has already anticipated this and created laravel/browser-kit-testing. The 5.4 documentation covers the basics for this in the upgrade guide. There are a couple of additional points that came up when I followed it:

  • You probably only want to pull this package in for development, in which case your command for requiring it should be:
composer require laravel/browser-kit-testing:1.* -dev
  • Update: This is no longer an issue, as an update has been made to the package to use the core traits.
    The traits that are in the Illuminate\Foundation\Testing namespace will not be detected by Laravel\BrowserKitTesting\TestCase. An issue was raised on github regarding this, and I have posted a couple of different approaches to resolving this on that thread.

Change to factory models

This is covered in the update guide, but I think it’s worth highlight as it’s likely to affect testing specifically. There is a change to the default behaviour of the factory function. Calling it (for example) asfactory(User::class, 1)->create() will return a collection containing a single User object, rather than the object itself that it returned in 5.3.

This is simple enough to resolve of course, just change the call to factory(User::class)->create() instead, and you will get the single User object you would have got before.

Clear your caches

I hit a couple of really odd bugs after my initial changes. One that really threw me was as follows:

Call to undefined method Illuminate\View\Factory::getFirstLoop()

It turned out that I had compiled view files from 5.3. I ran php artisan cache:clear but this wasn’t sufficient, so in the end I emptied the view cache directory manually instead rm -fR storage/framework/views/* and that resolved the problem.

Update: As noted in the responses (thanks Nick Wiersma), you should actually run php artisan view:clear which is a little more consistent with the tool set.

Namespacing your tests

Up until now, I have pretty much followed the convention that tests were not namespaced. A small change to the composer file:

"autoload-dev": {
"classmap": [
"tests"
]
},

had covered it, and all my tests were uniquely named such that there were no collisions and everything worked nicely. However, Laravel 5.4 does use namespacing for tests, so I decided to take the plunge and throw them into my tests as well. This does come in handy for using php artisan dusk:make later on, as it will put your tests into the Tests\Browser namespace. If you decide to follow the same approach, a couple of things to watch out for:

  • Anywhere you’ve referenced a model class (or any other namespaced class for that matter) inline will fail, as it will be looking for App\ModelName within the test namespace. You can either prefix with a backslash to get to the root, or take the plunge and switch to appropriate use statements (which is what I prefer).
  • You will have to make sure to run composer dump-autoload after you make any namespacing changes, otherwise they won’t be picked up.

Setting up Dusk

The instructions for setting up Dusk are fairly comprehensive, but they make a couple of assumptions about running browser tests that don’t exactly mesh with how I have approached them in the past.

Configuring the Service Provider

Dusk exposes a web route that allows a user to be logged in without any authentication whatsoever. So it’s very important that you follow the instructions to only enable the Service Provider in your testing environments in your AppServiceProvider. Do not add it to you config file.

use Laravel\Dusk\DuskServiceProvider;

/**
* Register any application services.
*
* @return void
*/
public function register()
{
if ($this->app->environment('local', 'testing')) {
$this->app->register(DuskServiceProvider::class);
}
}

Setting up the environment

To enable different environment settings, Dusk will look for .env.dusk.{env} and move that to .env while the tests are running. You don’t need a lot of detail in there, but you may want to generate an app key for it. If you do, add the APP_KEY token to the file, and then run php artisan key:generate --env=dusk.local (change local for whatever environment you are considering). I would also suggest that you set up APP_DEBUG=TRUE for any issues you may run across during your test development.

Running the webserver

I prefer my tests to be hitting a separate webserver rather than any development environment I might be using. It prevents my development environment from getting filled up with random data, and keeps the process of testing and developing separate. To this end, I use the artisan serve command:

php artisan serve --env=dusk.local

At the moment I simply fire this up in a separate terminal window, but I am considering sorting this out as a command to wrap around the dusk call.

Database setup

I am a big fan of the ability to use sqlite as an in memory database for a lot of the tests. Once you are running the webserver and the tests in separate processes, this is no longer an option. To get around this, I use a common sqlite file for the database. To ensure this behaves consistently, I have setup a new config in the config/database.php file:

'connections' => [
'sqlite_testing' => [
'driver' => 'sqlite',
'database' => database_path('testing.sqlite'),
'prefix' => '',
],
//...
]

And then in my .env.dusk.local file I have:

DB_CONNECTION=sqlite_testing

Keeping it clean

I really like using the DatabaseMigration trait, as it ensures that every test you are running is working with the correction version of the database structure. This gets a little more complex when you have two processes using the same SQLite file. To resolves this, I created a new trait that sets up a clean database for each test.

namespace Tests;
use DatabaseSeeder;

trait InitialiseDatabaseTrait
{
protected static $backupExtension = '.dusk.bak';

/**
* Creates an empty database for testing, but backups the current dev one first.
*/
public function backupDatabase()
{
if (!$this->app) {
$this->refreshApplication();
}

$db = $this->app->make('db')->connection();
if (!file_exists($db->getDatabaseName())) {
touch($db->getDatabaseName());
}

copy($db->getDatabaseName(), $db->getDatabaseName() . static::$backupExtension);
unlink($db->getDatabaseName());
touch($db->getDatabaseName());

$this->artisan('migrate');
$this->seed(DatabaseSeeder::class);
$this->beforeApplicationDestroyed([$this, 'restoreDatabase']);
}

/**
* Paired with backupDatabase to restore the dev database to its original form.
*/
public function restoreDatabase()
{
// restore the test db file
if (!$this->app) {
$this->refreshApplication();
}
$db = $this->app->make('db')->connection();
copy($db->getDatabaseName() . static::$backupExtension,
$db->getDatabaseName());
unlink($db->getDatabaseName() . static::$backupExtension);
}
}

As a new trait, I simply extended the functionality of setUpTraits in DuskTestCase to ensure the backupDatabase method is called.

use InitialiseDatabaseTrait;//... other standard stuff in the DuskTestCasepublic function setUpTraits()
{
$this->backupDatabase();
parent::setUpTraits();
}

Session problems

Having done all this, I setup the example login test and ran php artisan dusk. Annoyingly it didn’t work. It took me a while to figure out why (this is where I worked out that having APP_DEBUG=TRUE in the .env file was a good idea), but essentially there seemed to be a problem with having sessions set up to be in memory. So instead of SESSION_DRIVER=array I have SESSION_DRIVER=file in my .env.dusk.local file.

Updating PHPUnit config

One final piece of the puzzle you will probably want to resolve will be to exclude the Dusk tests from your standard phpunit test runs. This is a simple exclude directive in your test suite config (in bold below):

<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
<exclude>./tests/Browser/</exclude>
</testsuite>
</testsuites>

Next Steps

I haven’t really had the chance to write any further tests at this stage, but will be spending some time playing with Dusk in the next few days. I already have one test in mind that I think will prove an interesting challenge.

I also have a project that made fairly extensive use of the modelizer/selenium project, so I will be looking at that quite soon to see whether it will be worth transitioning to Dusk or not.

And of course, there’s getting it running on Codeship. Which will hopefully be fairly straightforward …

More Dusk …

  1. Laravel Dusk Automation: getting things work on Codeship.
  2. Laravel Dusk on Homestead: running Dusk in a Homestead environment instead of on the host machine.
  3. Writing Tests with Laravel Dusk: My first pass at playing with the actual testing process.

--

--

Mike Smith
Mike Smith

Responses (6)