noeldemartin/laravel-dusk-mocking

Mock facades in Laravel Dusk tests.

v5.4.0 2019-09-11 19:14 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 How does it work? sections on this readme before using it.

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 definetly 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 to a different process (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 convetion 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.