Writing tests with Laravel Dusk

Mike Smith
5 min readJan 26, 2017

Having worked my way through getting Laravel Dusk working on my current project (with a quick sidestep into getting it working on Homestead as well), I figured it was finally time to start working with Dusk to write some actual tests.

My Frontend Testing Background

On earlier projects I have worked on, there was a whole bunch of pain points to getting selenium up and running, and having the tests developed. In all honesty, I steered clear a lot of the time and let other people take care of it. Not a great approach to things, and one which I came to regret as things became more complex, and more obtuse.

It was great to see so much work being done with testing in Laravel when I started to work with it, and in Laravel 5.1, I used the laracasts/integrated package to build out some Selenium tests. With Laravel 5.3, it became clear that the selenium side of things was not something that was getting a lot of love, but then I discovered the modelizer/selenium package, and used that for my frontend tests.

And now we have Dusk, which appears to be trying to take the front end more seriously. So I’ve been taking it seriously, and implementing my front-end tests with it in my latest project.

The first test

The install step for Dusk sets up an initial test that you can immediately use to verify whether you’ve got things working or not, which is all very nice, but doesn’t really feel like I am trying out anything I can’t already do with the browser-kit-testing. So I immediately wanted to get into something a little more complex.

I have a form for creating an invoice. I want to check that if the user clicks on the add item button, another invoice item row is added to the form. So something like:

/** @test */
public function can_add_an_invoice_item_row()
{
$user = factory(User::class)->create();

$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/invoice/create')
->assertEquals(1, $browser->getItemCount())
->press('Add Item')
->assertEquals(2, $browser->getItemCount())
});
}

Obviously the getItemCount method is not something that exists on the standard Browser object, but there is a mechanism for defining that already, which is to define it on a Page object.

It further transpires that the assertions available to the Browser object does not reflect everything you might be used to from PHPUnit. The documentation provides the full list of assertions available, none of which are really going to help me. One simple approach to this would be add the assertion to the page object:

use PHPUnit_Framework_Assert as PHPUnit;// ...public function assertItemCountIs(Browser, $count)
{
PHPUnit::assertEquals($count, $this->getItemCount($browser));
}

I also need to add the getItemCount method to the Page object. The InteractsWithElements trait reveals the different ways in which I can get to the elements on the page. To get all the elements that match a particular selector, I can simply call the elements method:

public function getItemCount(Browser $browser)
{
return count($browser->elements('.item-row'));
}

However, this all feels a bit specific, and there are several clear improvements I wanted to make to this.

Defining element selectors

The Page object provides a handy means of defining the selectors you use for certain elements on the page. The goal of this is to ensure that if you change the structure of your page, you only need to the change the selector in one place. Having seen xpath selectors strewn throughout automated tests in the past, this is something I am keen to leverage from a very early stage of testing.

class CreateUserInvoice extends BasePage
{
public function elements()
{
return [
'@invoice-item-rows' => '.item-row',
];
}
public function assertItemCountIs(Browser $browser, $count)
{
PHPUnit::assertCount($count, $browser->elements('@invoice-item-rows'));
}

Which then leaves my test looking like this:

$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(new CreateUserInvoice())
->assertItemCountIs(1)
->press('Add Item')
->assertItemCountIs(2);
});

Expanding my assertions

One thing I noticed when looking at the Browser object is that it appears to Macro-able. Which made me think that rather than just having a very specific assertion on the Page object, it might be better to define new assertions through Macros, so that I could expose more assertions across all my tests in a consistent fashion.

A digression into Macro definition

The most straightforward way to define the macro would be to stick it directly into the AppServiceProvider, but I prefer to have my own. You can either create a MacroServiceProvider and then have that require_once various files that contain your macros, or you could create a DuskBrowserServiceProvider (DuskServiceProvider is already in use for Dusk itself) that contains the various macros you want to create.

I initially was going to go for the former approach, but realised I would only want to include these macro definitions when we are also including the DuskServiceProvider, so following a similar approach seemed sensible. So, I ran php artisan make:provider DuskBrowserServiceProvider. To ensure it was only loaded at the same time as Dusk, I edited my AppServiceProvider:

public function register()
{
if ($this->app->environment('local', 'testing')) {
$this->app->register(DuskServiceProvider::class);
$this->app->register(DuskBrowserServiceProvider::class);
}
}

And then in the boot method of my new service provider I added the following:

public function boot()
{
Browser::macro('assertCount', function($expectedCount, $haystack, $message = '') {
PHPUnit::assertCount($expectedCount, $haystack, $message);
return $this;
});
}

Note that I followed the variable naming that PHPUnit uses. Further note that it is returning $this at the end of the call. This ensures that we can use the assertion in the same way as all the other Browser assertions (the Macroable trait handles the binding of our Browser instance).

The final test

So, having done all this extra fiddling, my test can now look like:

$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit(new CreateUserInvoice())
->assertCount(1, $browser->elements('@invoice-item-rows'))
->press('Add Item')
->assertCount(2, $browser->elements('@invoice-item-rows'));
});

If my interactions with adding items to the page gets more involved, then I might wind up abstracting that into a method on the page as well, but for now, I think that provides a nicely expressive test where it’s clear what my assertions are expecting, and why.

Conclusion

In essence Dusk is a convenience wrapper for front-end testing, trying to take some of the pain away from the setting up of the process. After wrestling with Codeship and Homestead, it’s clear that there is still some work to be done to get rid of all the pain. But once it’s running, it seems to behave in a very consistent fashion. And if you happen to following the happy path of OS X and Valet, the whole thing is basically a doddle.

Getting into the tests themselves, I was initially a little concerned that Dusk might be quite limiting as to what I could actually test without it becoming quite unreadable. The realisation that the Browser object is Macroable has laid that concern to rest, and I’m now keen to push on with expanding my front-end test suite with Dusk.

Hopefully I’ll be able to share a few more tips and tricks along the way.

Related

--

--