noeldemartin/laravel-dusk-mocking

Mock facades in Laravel Dusk tests.

v6.5.2 2021-03-15 19:47 UTC

This package is auto-updated.

Last update: 2024-04-16 02:18:37 UTC


README

When running browser tests with Laravel Dusk, it is not possible to mock facades like it is usually done for http tests. This package aims to provide that functionality. However, it does so by doing some workarounds. It is recommended to read the Disclaimer (and in particular the Limitations) before using it.

Before adding it to your project, you can also give it a try with a bare-bones Laravel application prepared with tests running on a CI environment here: laravel-dusk-mocking-sandbox.

Installation

Install using composer:

composer require --dev noeldemartin/laravel-dusk-mocking

Add the following code to your base test case (usually DuskTestCase).

use NoelDeMartin\LaravelDusk\Browser;

...

protected function newBrowser($driver)
{
    return new Browser($driver);
}

Usage

The conceptual usage is the same as can be learned on Laravel's documentation about mocking. The only difference is that in Dusk, mocking can be set up independently on each browser instance. For that reason, instead of calling static methods we will call instance methods. Look at the following example on how to mock the Mail facade:

public function testOrderShipping()
{
    $this->browse(function ($browser) use ($user) {
        $mail = $browser->fake(Mail::class);

        $browser->visit('...')
                // Perform order shipping...
                ->assertSee('Order purchased! Check your email for details!');

        $mail->assertSent(OrderShipped::class, function ($mail) use ($order) {
            return $mail->order->id === $order->id;
        });

        // Assert a message was sent to the given users...
        $mail->assertSent(OrderShipped::class, function ($mail) use ($user) {
            return $mail->hasTo($user->email) &&
                   $mail->hasCc('...') &&
                   $mail->hasBcc('...');
        });

        // Assert a mailable was sent twice...
        $mail->assertSent(OrderShipped::class, 2);

        // Assert a mailable was not sent...
        $mail->assertNotSent(AnotherMailable::class);
    });
}

Notice how the api is the same as Http tests.

Configuration

Drivers serialize mocking data through requests, and cookies are used by default. The drawback is that using cookies, there is a size limit for how much information can be stored (4KB). For that reason, different drivers can be configured. Create a file named dusk-mocking.php inside your application config folder to change the default driver:

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Driver
    |--------------------------------------------------------------------------
    |
    | This option controls the default driver for storing mock data.
    |
    | Supported: "cookies", "session"
    |
    */
    'driver' => 'session',

];

Script timeout

As explained below, each test may send multiple requests to the application to serialize/deserialize fakes. In order to prevent changing the state of the browser, this is done using javascript XHR requests. These requests will timeout in 1 second by default, but this can be configured using the $javascriptRequestsTimeout variable:

protected function newBrowser($driver)
{
    Browser::$javascriptRequestsTimeout = 5;

    return new Browser($driver);
}

Extending

Doing this, only the functionality analogous of calling fake is available, and not methods mocking. In order to mock custom facades or modify Laravel's default fakes, the system can be extended by using custom fakes. Those can be registered in two ways:

  • Registered globally (every test using fakes will use them). Add the following to your base test case (usually DuskTestCase):
public function setUp()
{
    parent::setUp();

    // Register fake mocks
    Mocking::registerFake(Mail::class, MyMailFake::class);
}
  • Another option is to register them only for one browser. This can be useful if it's necessary to have different fakes for multiple browsers or for aesthetic reasons (performance is not affected either way):
$this->browse(function (Browser $browser) {
    $browser->registerFake(Mail::class, MyMailFake::class);

    $mail = $browser->mock(Mail::class);

    // Proceed with the test
});

In order to understand how to implement these Fake classes, you can take a look at how Laravel fakes are implemented, since those are the ones used by default. This classes can also be used as a base, extending them and adding any modifications.

Disclaimer

Most scenarios should be covered with http tests, since they both run faster and are more reliable when testing your code. The same could be said when testing your frontend and Javascript, there are multiple frameworks specific for that. However, there is definitely some value on using Dusk for integration & end to end tests. For those scenarios it's rarely necessary to mock any facades. But if you do find yourself in that situation, this package can help you 😃.

How does it work?

The reason why mocking cannot be done like in normal http tests is because when Dusk runs a test it's really doing an actual request using a browser (running for example on chromedriver). Server and client don't share the same runtime, and that's why it isn't possible to have code communicate between them. Knowing this, how does this package work? Well, Dusk is already achieving something similar to this when using authentication. Some special routes (as a convention starting with _dusk) are created to provide communication with the server process, and state is persisted in the session like it would with a normal Laravel session. Given this and other uses, different drivers can be configured for tests (in your .env.dusk or phpunit.dusk.xml files). By using a separate session driver such as a testing database, it can be wiped out before and after each test to guarantee a real black-box scenario.

In a nutshell, what happens under the hood is that Facades are replaced using the swap method at the beginning of every request, and they'll be serialized at the end. When calling assertions methods from test code, data will be deserialized into the test runtime and assertions executed as usual.

You can learn more about how this works looking at MockingProxy, Driver and MockingServiceProvider classes.

Limitations

The serialization/deserialization of mocked services is implemented using php's serialize and unserialize functions. One limitation that this ensues is that closures can't be serialized. If you see an error saying Serialization of 'Closure' is not allowed, that means somewhere inside a service you're faking, there is a closure.

There is a couple of things you could do to work around this limitation.

If you have control over the service, you could use the SerializableClosure class to make your closures serializable (Laravel core is already doing this in multiple places).

If you're trying to mock a 3rd party service you don't have control over, you could implement a service fake yourself instead of using the built-in class. This library already does it with some common Laravel services, but it doesn't cover the complete Laravel api. You can register your custom fakes using the extending functionality. Although this may seem a daunting task, keep in mind that you only need to implement fakes for the functionality you're testing using this package. And as we discussed at the start of this section, that shouldn't be a lot.