Feb 3, 2017 | laravel 5.4, Dusk

Introducing Laravel Dusk (new in Laravel 5.4)

Series

This is a series of posts on New Features in Laravel 5.4.

!
Warning: This post is over a year old. I don't always update old posts with new information, so some of this information may be out of date.

If you follow anyone in the Laravel world on Twitter, or if you listen to the Laravel Podcast, you know by now that Laravel Dusk is the new face of application testing in the Laravel world.

The background of application testing in Laravel

First, a quick refresher: while everyone talking about testing uses words a little bit differently, it's pretty well agreed that Unit tests are responsible for testing little chunks of code (a single method on a single class, for example) in isolation, whereas Application tests (similar, or the same as, integration tests) test the entire application as a whole.

Since Jeffrey Way's "Integrated" package was brought into the core in Laravel 5.1, we've had access to methods like ->visit(), ->get(), ->see(), etc.—making it seem like we were describing the actions of a browser visiting the site. This really transformed our ability to write application tests, making calls like this possible:

    /** @test */
    public function cta_link_functions()
    {
        $this->visit('/sales-page')
            ->click('Try it now!')
            ->see('Sign up for trial')
            ->onPage('trial-signup');
    }

In the background, it was really PHP spinning up a request, passing it through our application, crawling the DOM, and then making more requests until the chain is done. There was no browser. But it felt like it.

The problem with the old way

What if any of your application's functionality relied on JavaScript, though? Sorry. Out of luck. Because this isn't a real browser, it didn't know or care about your JavaScript.

Over time, the desire to use and test JavaScript components in our Laravel apps grew, and so did the discontent that there was a growing number of o applications that were un-testable using the tools Laravel provided out of the box.

The solution: Laravel Dusk

With Dusk, Taylor has completely re-written how application testing works in Laravel. Everything is now based on a tool called ChromeDriver, which is a standalone server that actually controls Chrome/Chromium. When you write application tests, Dusk sends your commands to ChromeDriver, which then spins up Chrome to run your tests in the browser and then reports back the results.

All of the non-application testing aspects of Laravel–its unit testing functionalities, and HTTP-request-based tests like $this->get()–are still using the same code they always were. But the more advanced features like $this->visit() just don't work at all out of the box. It's up to you to pull in an application testing package. You can either pull in Dusk (composer require laravel/dusk --dev) or you can pull in the pre-5.4 application testing package (composer require laravel/browser-kit-testing --dev).

Note: if you pull in Browser Kit Testing, you'll need to modify your TestCase to extend Laravel\BrowserKitTesting\TestCase instead of Illuminate\Foundation\Testing\TestCase. Upgrading your test suite from a pre-5.4 app? Check out Adam Wathan's Upgrading Your Test Suite for Laravel 5.4.

Getting started with Dusk

Once you've brought Dusk into your application (composer require laravel/dusk --dev), you'll need to register the service provider. You could add it to the list of service providers in config/app.php, but that's not actually safe–Dusk, for the purpose of testing, opens up a lot of manual overrides that you don't want on your production site. Instead, conditionally register it in the register method of AppServiceProvider:

// AppServiceProvider
use Laravel\Dusk\DuskServiceProvider;

...

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

Now we need to install Dusk, which will create a tests/Browser directory.

php artisan dusk:install

You may never have used the APP_URL key in your .env file—it's often not actually necessary for many applications–but you'll need to set it now, since Dusk relies on it to visit your application. This will have to be an actually-accessible URL, because, remember, this is a real browser we're working with.

We now run our tests using php artisan dusk, which can accept any arguments that PHPUnit can–for example, php artisan dusk --filter=the_big_button_works.

Writing our first Dusk test

Let's say I want to write a Dusk test just like our application test we looked at earlier–click a button and make sure it takes me where I want to go. Let's write.

php artisan dusk:make BigButtonTest

Let's open tests/Browser/BigButtonTest.php and see what we get by default:

<?php

namespace Tests\Browser;

use Tests\DuskTestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class BigButtonTest extends DuskTestCase
{
    /**
     * A Dusk test example.
     *
     * @return void
     */
    public function testExample()
    {
        $this->browse(function ($browser) {
            $browser->visit('/')
                    ->assertSee('Laravel');
        });
    }
}

A few things you'll notice that are different from what we're used to.

First, we have namespaces in our tests now! This is actually true in 5.4 whether or not you're using Dusk; by default there are two namespaces for our tests, Tests\Unit and Tests\Feature.

Second, DatabaseTransactions and WithoutMiddleware aren't imported by default anymore.

Third, we're no longer calling $this->visit directly. We're now doing all of our testing in a closure in the context of the browse() function, which encapsulates our Dusk calls.

Fourth, some of the methods available to us have been renamed to be a little more consistent with other assertions–for example, see() is now assertSee().

Before we do anything else, let's just run our test. Remember, that's php artisan dusk.

Dusk running

Did you see that? Real. Browser. Windows.

Let's make this do what it did before:

$this->browse(function ($browser) {
    $browser->visit('/sales-page')
        ->clickLink('Try it now!')
        ->assertSee('Sign up for trial')
        ->assertPathIs('/trial-signup');
});

OK. We can do what we once could do. This is good. But what's new?

New interactions

There are so many new features and new ways of interacting that I'm going to have to point you to the docs to learn everything. But here are a few pieces that are really distinctly different from how application testing used to work, so you can get a sense of what we're working with here.

Getting and setting the text, value, or attributes of elements on the page

It's possible to get or set the value (the "value" property) and get the text (text contents) or attribute of any element on the page given its jQuery-style selector.

// Get or set
$inputValue = $browser->value('#name-input');
$browser->value('#email-input', 'matt@matt.com');

// Get
$welcomeDivValue = $browser->value('.welcome-text');
$buttonDataTarget = $browser->attribute('.button', 'data-target');

Interacting with forms and other page elements

Interacting with forms is very similar to what it was like previously, but let's cover it briefly. First, you can choose the name of the field if you'd like, or a jQuery-style selector.

$browser->type('email', 'matt@matt.com');
$browser->type('#name-input', 'matt');

You can clear any values:

$browser->clear('password');

You can select a dropdown value:

$browser->select('plan', 'premium');

You can check a checkbox or radio button:

$browser->check('agree');
$browser->uncheck('mailing-list');
$browser->radio('referred-by', 'friend');

You can attach files:

$browser->attach('profile-picture', __DIR__ . '/photos/user.jpg');

And you can even perform more complex keyboard- and mouse-based interactions:

// type 'hype' while holding the command key
$browser->keys('#magic-box', ['{command}', 'hype']);

$browser->click('#randomize');

$browser->mouseover('.hover-me');

$browser->drag('#tag__awesome', '.approved-tags');

Finally, we can scope any of our actions to a particular form or section of the site we're working on:

$browser->with('.sign-up-form', function ($form) {
    $form->type('name', 'Jim')
        ->clickLink('Go');
});

Waiting

This is probably the most foreign concept in Dusk. Because this is a real browser, it actually has to load all of the external assets on the page–which means your content may not be ready.

There are a few methods that help you work around this. First, you can just pause the test manually:

// Pause for 500ms
$browser->pause(500);

More commonly, you can wait (by default, up to 5 seconds) until a given element either appears or disappears:

$browser->waitFor('.chat-box');

// wait a maximum of 2 seconds for the chat box to appear
$browser->waitFor('.chat-box', 2);

$browser->waitUntilMissing('.loading');
$browser->waitForText('You have arrived!');
$browser->waitForLink('Proceed');

// wait and scope
$browser->whenAvailable('.chat-box'), function ($chatBox) {
    $chatBox->assertSee('What is your message?')
        ->type('message', 'Hello!')
        ->press('Send');
});

// wait until JavaScript expression returns true
$browser->waitUntil('App.initialized');

Creating multiple browsers

Taking an example from the docs, what if you want to test that a websocket-based chat works? Just use two separate browser sessions:

$this->browse(function ($first, $second) {
    $first->loginAs(User::find(1))
          ->visit('/home')
          ->waitForText('Message');

    $second->loginAs(User::find(2))
           ->visit('/home')
           ->waitForText('Message')
           ->type('message', 'Hey Taylor')
           ->press('Send');

    $first->waitForText('Hey Taylor')
          ->assertSee('Jeffrey Way');
});

As you can see, if you ask for more parameters in your browse() closure, each will be passed a new browser session that you can interact with.

Our first browser logs in as user 1, visits the home route, and then waits (for up to 5 seconds) until it sees the text "Message," which in this test is representing the chat box appearing on the page. Next, our other user logs in as user 2, visits the home route, waits to see the chat box, and then types a message into it and hits send. Finally, our original user watches for that message to come through and asserts that the name of the second user (which we are presuming is named "Jeffrey Way") shows up.

Also note that loginAs, which used to be named be() or actingAs(), can take either a User instance or a user ID.

New assertions

Most of the assertions in Dusk are the same as before, but many have new names, and there are a few new ones. Check the whole list here, but here are a few notable new assertions:

$browser->assertTitle('My App - Home');
$browser->assertTitleContains('My New Blog Post');
$browser->assertVisible('.chat-box');
$browser->assertMissing('.loading');

Dusk Pages

Reading longer and more complex sets of Dusk interactions can be hard to follow at times, so there's an optional concept called a Page that makes it easy to group functionality in your Dusk tests. A page represents a URL that can be used to navigate to it, a set of assertions that can be run to make sure the browser is still on this page, and a set of nicknames for common selectors.

Creating a Page

To make a page, use the dusk:page Artisan command:

php artisan dusk:page Dashboard

Here's what that generates for us:

<?php

namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;
use Laravel\Dusk\Page as BasePage;

class Dashboard extends BasePage
{
    /**
     * Get the URL for the page.
     *
     * @return string
     */
    public function url()
    {
        return '/';
    }

    /**
     * Assert that the browser is on the page.
     *
     * @return void
     */
    public function assert(Browser $browser)
    {
        $browser->assertPathIs($this->url());
    }

    /**
     * Get the element shortcuts for the page.
     *
     * @return array
     */
    public function elements()
    {
        return [
            '@element' => '#selector',
        ];
    }
}

The url() method is clear: it tells how to navigate to this page. The assert() method is also relatively clear: "Consider me still on this page as long as this assertion passes."

The elements() array makes it possible to create shorthand selectors you can use to refer to elements any time your browser is "on" this page. Here's a way we might choose to fill this out:

class Dashboard extends BasePage
{
    public function url()
    {
        return '/dashboard';
    }

    public function assert(Browser $browser)
    {
        $browser->assertPathIs($this->url());
    }

    public function elements()
    {
        return [
            '@createPost' => '#create-new-post-button',
            '@graphs' => '.dashboard__graphs',
        ];
    }
}

You can also manually create custom methods for interactions on each page. For example, one common behavior in your tests might be to set a few dropdowns and then click a "filter" button. Let's make it:

// Dashboard
public function filterGraph($browser, $filterStatus)
{
    $browser->select('filterBy', $filterStatus)
        ->select('limit', 'one-month')
        ->press('Filter');
}

Using a Page

There are a few different ways we can use a Page. First, we can visit it, which both directs the browser to it and also loads our shorthand selectors:

use Tests\Browser\Pages\Dashboard;
...

$browser->visit(new Dashboard)
    ->assertSee('@graphs');

But what if we're already on this page because we clicked a button somewhere else? The on() method loads up our Page:

use Tests\Browser\Pages\Dashboard;
...

$browser->visit('/)
    ->type('email', 'matt@matt.com')
    ->type('password', 'secret')
    ->press('Log in')
    ->on(new Dashboard)
    ->assertSee('@graphs');

Finally, here's how we use our custom methods:

$browser->visit(new Dashboard)
    ->filterBy('donors')
    ->assertSee('Sally');

Global shorthand selectors

You can also create global shorthand selectors you can use anywhere in your site in the default tests/Browser/Pages/Page Page, which is loaded on every page. Just add them to its siteElements() method.

// tests/Browser/Pages/Page
public static function siteElements()
{
    return [
        '@openChat' => '#chat-box__open-button',
    ];
}

Miscellany

OK, so you've seen how powerful this all is. A few side notes.

First, you can create a custom Dusk environment file at .env.dusk.local (or .env.dusk.whateverEnvironmentYouWantToTest).

Second, some of the methods require jQuery to select content on the page. Dusk will check whether your page loads jQuery, and if not, will inject it for you during the tests.

Finally, any time a test fails, Dusk will take a screenshot of the failed page for you and put it in the tests\Browser\Screenshots directory. You'll see exactly what the page looks like:

A failed Dusk test screenshot

Clude-con

That's all, folks. Enjoy! Remember, you can still keep writing the tests you've always written–and you can even pull in the old testing package, if you'd prefer. But there's a whole new world open to you now. Try it out a bit.


Comments? I'm @stauffermatt on Twitter


Tags: laravel 5.4  •  Dusk


This is part of a series of posts on New Features in Laravel 5.4:

  1. Feb 3, 2017 | laravel 5.4, Dusk
  2. Feb 8, 2017 | laravel, laravel 5.4, mix
  3. Aug 22, 2017 | laravel, laravel 5.4, Facades

Subscribe

For quick links to fresh content, and for more thoughts that don't make it to the blog.