Mocking

Mocking

ℹ️ Si necesitas ver los ejemplos en phpunit y en pest, navegar hasta la sección de la documentación oficial

Cuando testeas una aplicación de Laravel, puedes desear ‘mockear’ ciertos aspectos de tus aplicación por lo que no serán ejecutados durante un test. Por ejemplo cuando testeas un controlador que lanza un evento(dispatch), puedes desear que no se ejecute el listener que está escuchando por dicho Evento. Esto te permite solo testear el controlador Http sin preocuparte por la ejecución del listener, dado que el listener será testeado en su propio caso de prueba

Laravel provee metodos para mockear eventos, jobs, facades,… Estos helpers principalmente proveen una capa conveniente por encima de Mockery, por lo que no lo tendrás que hacer manualmente tú.

Mocking Objects

Cuando creas un mock de un objeto que va a ser inyectado en tu aplicación usando el service container de Laravel, necesitarás bindear tu instancia mockeada en el contenedor usando el método instance . Esto le dirá a Laravel que use la instancia mockeada en vez de construir el objeto por si mismo.

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();
        })
    );
});

Para hacer esto mas conveniente, puedes usar el metodo mock que realiza lo mismo que el código de arriba

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

Puedes usar partialMock si solamente necesitas mockear algunos de los métodos del objeto. Los métodos no mockeados se usarán como siempre.

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

De manera similar, si necesitas un spy de un objeto, la clase base de Laravel de test ofrece un método wrapper que envuelve el método Mockery::spy . Los spies son similares a los mocks, sin embargo, los spies graban(record) cualquier interacción entre el spy y el código testeado, permitiendo realizar aserciones después que el código se haya ejecutado

Mocking Facades

A diferencia de los métodos estáticos, las facades (incluyendo real-time facades) pueden ser mockeados. Esto provee una gran ventaja sobre los métodos estaticos tradicionales y otorga la misma testability (capacidad de testeo) que usando la inyección de dependencia tradicional. Cuando testeas, puedes necesitar mockear una llamada a una facade de Laravel, Por ejemplo:

<?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 [
            // ...
        ];
    }
}

Podemos mockear la llamada a la facade Cache usando el método shouldReceive , el cual devolverá una instancia de Mockery. Dado que las facade son resueltas y manejadas por el service container de Laravel, tienen mucha más capacidad de testeo que una típica clase estática. Por ejemplo vamos a mockear la llamada al metodo get de Cache

<?php
 
use Illuminate\Support\Facades\Cache;
 
test('get index', function () {
    Cache::shouldReceive('get')
                ->once()
                ->with('key')
                ->andReturn('value');
 
    $response = $this->get('/users');
 
    // ...
});

❗❗ No deberías mockear la facade de Request. En cambio, pasa el input que deseas en los metodos get y post de los metodos Http para testing (HTTP testing methods) cuando corras los tests. De la misma forma, en vez de mockear la facade Config , llama al método Config::set en tus tests

Facade Spies

Si necesitas crear un spy de una facade, puedes llamar al método spy de la correspondiente facade.

<?php
 
use Illuminate\Support\Facades\Cache;
 
test('values are be stored in cache', function () {

    Cache::spy();
 
    $response = $this->get('/');
 
    $response->assertStatus(200);
 
    Cache::shouldHaveReceived('put')->once()->with('name', 'Taylor', 10);
});

Interacting With Time

Cuando testeas, puedes necesitas modificar el timepo devuelto por los helpers now o Illuminate\Support\Carbon::now() . Por suerte, la clase base de test de Laravel incluye helper que permiten manipular el time actual:

test('time can be manipulated', function () {
    // Travel into the future...
    $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();
 
    // Travel into the past...
    $this->travel(-5)->hours();
 
    // Travel to an explicit time...
    // Puedes viajar a un time concreto
    $this->travelTo(now()->subHours(6));
 
    // Return back to the present time...
    $this->travelBack();
});

Puedes pasar tambien una closure a varios metodos travel. El closure será invocado con tiempo congelado en el tiempo especificado. Una vez el closure se ha ejecutado, el time seguirá corriendo normal.

$this->travel(5)->days(function () {
    // Test something five days into the future...
});
 
$this->travelTo(now()->subDays(10), function () {
    // Test something during a given moment...
});

El metodo freezeTime puede ser usado para congelar el time actual. De manera similar, el método freezeSecond congelará el tiempo pero al principio del segundo actual.

use Illuminate\Support\Carbon;
 
// Freeze time and resume normal time after executing closure...
$this->freezeTime(function (Carbon $time) {
    // ...
});
 
// Freeze time at the current second and resume normal time after executing closure...
$this->freezeSecond(function (Carbon $time) {
    // ...
})

Como podrás imaginar, los métodos mostrados arriba son principalmente usados para testear el comportamiento de aplicaciones con time sensitive, como por ejemplo, cómo bloquear posts inactivos en un foro de discusión.

use App\Models\Thread;
 
test('forum threads lock after one week of inactivity', function () {
    $thread = Thread::factory()->create();
 
    $this->travel(1)->week();
 
    expect($thread->isLockedByInactivity())->toBeTrue();
});