Write prettier Laravel Dusk tests by abusing a PHPUnit data provider

Dusk tests, just like your application code, deserve to look their best. Unfortunately, typical Dusk tests can be quite unsightly. Consider this example from the documentation:

public function test_login_as_user()
{
    $user = factory(User::class)->create();

    $this->browse(function (Browser $browser) use ($user) {
        $browser->visit('/login')
            ->type('email', $user->email)
            ->type('password', 'password')
            ->press('Login')
            ->assertPathIs('/home');
    });
}

You have to call $this->browse() with a closure to get a browser. The closure adds an extra level of indentation to your code. Any variables defined outside the closure have to be passed into the closure with a use. All that trouble just to get a browser, something we have to do every test...

Our life would be a lot simpler if we could somehow get the browser without having to call $this->browse(). Tests could look like this instead:

public function login_as_user(Browser $browser)
{
    $user = factory(User::class)->create();

    $browser->visit('/login')
        ->type('email', $user->email)
        ->type('password', 'password')
        ->press('Login')
        ->assertPathIs('/home');
}

But how can we inject the browser into the test?

Abusing a data provider

The test above is very similar to what a test that uses a data provider looks like. That got me thinking, can we get a data provider to give us a browser?

As it turns out, yes we can! We can get PHPUnit to inject the browser in our Dusk tests, all we need is a little bit of reflection. This sounds pretty ridiculous, but it actually works great.

All we have to do is add the following two methods to our DuskTestCase:

<?php

namespace Tests\Browser;

use Laravel\Dusk\Browser;
use ReflectionClass;
use ReflectionMethod;

abstract class DuskTestCase
{
    public function providesTests()
    {
        $reflection = new ReflectionClass($this);

        foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
            $parameters = $method->getParameters();

            if (count($parameters) === 0) {
                continue;
            }

            $firstParameterType = $parameters[0]->getClass()->name ?? null;

            if ($firstParameterType === Browser::class) {
                yield $method->name => [$method->name];
            }
        }
    }

    /** @dataProvider providesTests */
    function test($method)
    {
        $this->browse(function (Browser $browser) use ($method) {
            $this->{$method}($browser);
        });
    }
}

How it works

The first step is discovering all our tests. The providesTests data provider uses reflection to look at all public methods within the class. All of our tests should already be extending the DuskTestCase, which means the reflection will discover them all.

The second step is checking which tests want to receive a browser. We can do that by checking the type of the method's first parameter. If the method is public, and wants a browser, we'll assume it is a pretty Dusk test.

The data provider will run the test method once for each discovered test, and pass in the test method name. The test method calls the $this->browse() method, and then calls our pretty Dusk test.

As a result, we now don't have to call $this->browse() in our Dusk tests anymore!

The downside

The only downside to this approach is that you have to make sure that these pretty tests aren't discovered by PHPUnit itself. Our data provider is already discovering them, so PHPUnit doesn't have to. If PHPUnit discovers these tests, it won't inject the browser, and the test will always fail. Just make sure your pretty Dusk tests don't have a name that starts with test, and that they dont have a /** @test */ annotation, and you are good to go.

In closing

Dusk tests can be burdensome to write, but they are invaluable for making sure your application works correctly. I've been writing my Dusk tests like this for a few months now, and I haven't seen a reason not to use this approach. Making your tests a little prettier can help keep you sane while writing them.