Mocking

Introduzione

Quando si testano applicazioni Laravel, potrebbe essere utile "mockare" alcuni aspetti dell’applicazione in modo che non vengano eseguiti realmente durante un determinato test. Ad esempio, quando si testa un controller che invia un evento, potrebbe essere necessario mockare gli ascoltatori dell’evento in modo che non vengano eseguiti durante il test. Questo permette di testare solo la risposta HTTP del controller senza preoccuparsi dell’esecuzione degli ascoltatori, poiché gli ascoltatori possono essere testati nel loro caso di test specifico.

Laravel offre metodi utili per mockare eventi, job e altre facades direttamente. Questi helper forniscono principalmente uno strato di comodità su Mockery, così non è necessario effettuare manualmente chiamate complesse a Mockery.

Mocking degli Oggetti

Quando effettui il mocking di un oggetto che verrà iniettato nella tua applicazione tramite il service container di Laravel, dovrai collegare la tua istanza mock al container come un binding instance. Questo istruirà il container a utilizzare la tua istanza mock dell’oggetto invece di costruire l’oggetto stesso:

use App\Service;
use Mockery;
use Mockery\MockInterface;

test('something can be mocked', function () {
    $this->instance(
        Service::class,
        Mockery::mock(Service::class, function (MockInterface $mock) {
            $mock->shouldReceive('process')->once();
        })
    );
});
use App\Service;
use Mockery;
use Mockery\MockInterface;

public function test_something_can_be_mocked(): void
{
    $this->instance(
        Service::class,
        Mockery::mock(Service::class, function (MockInterface $mock) {
            $mock->shouldReceive('process')->once();
        })
    );
}

Per rendere questo più conveniente, puoi usare il metodo mock fornito dalla classe base del caso di test di Laravel. Ad esempio, il seguente esempio è equivalente a quello sopra:

    use App\Service;
    use Mockery\MockInterface;

    $mock = $this->mock(Service::class, function (MockInterface $mock) {
        $mock->shouldReceive('process')->once();
    });

Puoi usare il metodo partialMock quando hai bisogno di mockare solo alcuni metodi di un oggetto. I metodi che non sono mockati verranno eseguiti normalmente quando vengono chiamati:

    use App\Service;
    use Mockery\MockInterface;

    $mock = $this->partialMock(Service::class, function (MockInterface $mock) {
        $mock->shouldReceive('process')->once();
    });

Allo stesso modo, se vuoi creare spy su un oggetto, la classe base del caso di test di Laravel offre un metodo spy come comoda interfaccia al metodo Mockery::spy. Gli spy sono simili ai mock; tuttavia, gli spy registrano ogni interazione tra lo spy e il codice in fase di test, permettendoti di fare asserzioni dopo che il codice è stato eseguito:

    use App\Service;

    $spy = $this->spy(Service::class);

    // ...

    $spy->shouldHaveReceived('process');

Mocking di Facades

Diversamente dalle tradizionali chiamate a metodi statici, le facade (incluse le facade in tempo reale) possono essere simulate. Questo offre un grande vantaggio rispetto ai metodi statici tradizionali e garantisce la stessa testabilità che avresti se usassi l’iniezione delle dipendenze tradizionale. Durante i test, potresti spesso voler simulare una chiamata a una facade di Laravel che si verifica in uno dei tuoi controller. Ad esempio, considera la seguente azione del controller:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    /**
     * Retrieve a list of all users of the application.
     */
    public function index(): array
    {
        $value = Cache::get('key');

        return [
            // ...
        ];
    }
}

Possiamo simulare la chiamata alla facade Cache usando il metodo shouldReceive, che restituirà un’istanza di un mock di Mockery. Poiché le facade sono effettivamente risolte e gestite dal service container di Laravel, hanno molta più testabilità rispetto a una classe statica tipica. Ad esempio, simuliamo la nostra chiamata al metodo get della facade Cache:

<?php

use Illuminate\Support\Facades\Cache;

test('get index', function () {
    Cache::shouldReceive('get')
                ->once()
                ->with('key')
                ->andReturn('value');

    $response = $this->get('/users');

    // ...
});
<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Cache;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function test_get_index(): void
    {
        Cache::shouldReceive('get')
                    ->once()
                    ->with('key')
                    ->andReturn('value');

        $response = $this->get('/users');

        // ...
    }
}

Non dovresti simulare la facade Request. Invece, passa i dati di input desiderati ai metodi di test HTTP come get e post durante l’esecuzione del test. Allo stesso modo, invece di simulare la facade Config, chiama il metodo Config::set nei tuoi test.

Facade Spy

Se desideri creare spy su una facade, puoi chiamare il metodo spy sulla facade corrispondente. Gli spies sono simili ai mocks; tuttavia, registrano ogni interazione tra lo spy e il codice in fase di test, permettendoti di fare asserzioni dopo l’esecuzione del codice:

<?php

use Illuminate\Support\Facades\Cache;

test('i valori vengono memorizzati nella cache', function () {
    Cache::spy();

    $response = $this->get('/');

    $response->assertStatus(200);

    Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);
});
use Illuminate\Support\Facades\Cache;

public function test_i_valori_vengono_memorizzati_nella_cache(): void
{
    Cache::spy();

    $response = $this->get('/');

    $response->assertStatus(200);

    Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);
}

Interagire con il Tempo

Durante i test, potresti aver bisogno di modificare l’ora restituita da helper come now o Illuminate\Support\Carbon::now(). Fortunatamente, la classe di base per i test delle funzionalità di Laravel include helper che ti permettono di manipolare l’ora corrente:

test('il tempo può essere manipolato', function () {
    // Viaggia nel futuro...
    $this->travel(5)->milliseconds();
    $this->travel(5)->seconds();
    $this->travel(5)->minutes();
    $this->travel(5)->hours();
    $this->travel(5)->days();
    $this->travel(5)->weeks();
    $this->travel(5)->years();

    // Viaggia nel passato...
    $this->travel(-5)->hours();

    // Viaggia a un'ora specifica...
    $this->travelTo(now()->subHours(6));

    // Torna al tempo presente...
    $this->travelBack();
});
public function test_il_tempo_può_essere_manipolato(): void
{
    // Viaggia nel futuro...
    $this->travel(5)->milliseconds();
    $this->travel(5)->seconds();
    $this->travel(5)->minutes();
    $this->travel(5)->hours();
    $this->travel(5)->days();
    $this->travel(5)->weeks();
    $this->travel(5)->years();

    // Viaggia nel passato...
    $this->travel(-5)->hours();

    // Viaggia a un'ora specifica...
    $this->travelTo(now()->subHours(6));

    // Torna al tempo presente...
    $this->travelBack();
}

Puoi anche fornire una chiusura ai vari metodi di viaggio nel tempo. La chiusura verrà eseguita con il tempo congelato all’ora specificata. Una volta eseguita la chiusura, il tempo riprenderà normalmente:

    $this->travel(5)->days(function () {
        // Testa qualcosa cinque giorni nel futuro...
    });

    $this->travelTo(now()->subDays(10), function () {
        // Testa qualcosa in un momento specifico...
    });

Il metodo freezeTime può essere usato per congelare l’ora attuale. Allo stesso modo, il metodo freezeSecond congela l’ora corrente ma all’inizio del secondo attuale:

    use Illuminate\Support\Carbon;

    // Congela il tempo e riprende il tempo normale dopo aver eseguito la chiusura...
    $this->freezeTime(function (Carbon $time) {
        // ...
    });

    // Congela il tempo al secondo attuale e riprende il tempo normale dopo aver eseguito la chiusura...
    $this->freezeSecond(function (Carbon $time) {
        // ...
    });

Come ti aspetteresti, tutti i metodi discussi sopra sono principalmente utili per testare il comportamento dell’applicazione sensibile al tempo, come bloccare post inattivi in un forum di discussione:

use App\Models\Thread;

test('i thread del forum si bloccano dopo una settimana di inattività', function () {
    $thread = Thread::factory()->create();

    $this->travel(1)->week();

    expect($thread->isLockedByInactivity())->toBeTrue();
});
use App\Models\Thread;

public function test_i_thread_del_forum_si_bloccano_dopo_una_settimana_di_inattività()
{
    $thread = Thread::factory()->create();

    $this->travel(1)->week();

    $this->assertTrue($thread->isLockedByInactivity());
}
Lascia un commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *