Saltar al contenido principal

Por qué utilizar mocks

Al escribir pruebas hay procesos que no queremos ejecutar de verdad: enviar correos, leer o escribir la caché, llamar a APIs externas… Sustituirlos por «impostores que se hacen pasar por lo real» se llama mocking. Ventajas de usar mocks:
  • Los tests son rápidos: no se ejecutan servicios externos ni operaciones lentas, así que terminan enseguida.
  • Los tests son estables: no dependen del estado de un servicio externo y siempre dan el mismo resultado.
  • El alcance del test queda claro: verificas una única clase o método.
Laravel ofrece helpers listos para mockear eventos, jobs, facades, etc. Por dentro se apoya en Mockery, pero no necesitas una configuración compleja.

Objetos mock

Para mockear un objeto que se inyecta a través del contenedor, registra la instancia mock en él. Así, el contenedor usa el mock en lugar de crear el objeto real.
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->expects('process');
        })
    );
});
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->expects('process');
        })
    );
}

Método mock()

El método mock() de la clase base de tests de Laravel hace lo mismo con menos código.
use App\Service;
use Mockery\MockInterface;

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

Método partialMock()

Si solo quieres mockear algunos métodos, utiliza partialMock(). Los que no se mockeen se ejecutan con normalidad.
use App\Service;
use Mockery\MockInterface;

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

Método spy()

Un spy es parecido a un mock, pero verifica la interacción después de ejecutar el código. Mientras que en un mock defines de antemano «este método debería llamarse», con un spy compruebas a posteriori «¿se ha llamado este método?».
use App\Service;

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

// ...ejecuta el código que estás probando...

$spy->shouldHaveReceived('process');

Mockear facades

A diferencia de las llamadas estáticas normales, los facades sí se pueden mockear. Ofrecen la misma testabilidad que la inyección de dependencias con una sintaxis mucho más concisa. Considera este controlador que utiliza la caché.
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    /**
     * Devuelve el listado de usuarios de la aplicación
     */
    public function index(): array
    {
        $value = Cache::get('key');

        return [
            // ...
        ];
    }
}
Para mockear Cache::get, usa expects().
<?php

use Illuminate\Support\Facades\Cache;

test('get index', function () {
    Cache::expects('get')
        ->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::expects('get')
            ->with('key')
            ->andReturn('value');

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

        // ...
    }
}
No mockees el facade Request. En su lugar, pasa los datos de entrada a los métodos HTTP de prueba (get, post, etc.). De igual manera, en lugar de mockear el facade Config, llama a Config::set() dentro del test.

Spies de facade

Para monitorizar un facade con un spy, llama a su método spy(). Es útil cuando quieres verificar la interacción tras ejecutar el código.
<?php

use Illuminate\Support\Facades\Cache;

test('los valores se guardan en la caché', function () {
    Cache::spy();

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

    $response->assertStatus(200);

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

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

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

    $response->assertStatus(200);

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

Manipulación del tiempo

Cuando pruebas lógica que depende del tiempo, resulta útil poder cambiar la hora que devuelven now() o Carbon::now(). La clase base de los feature tests de Laravel incluye helpers para manipular el tiempo.

travel(): viajar en el tiempo

test('se puede manipular el tiempo', function () {
    // Avanzar
    $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();

    // Retroceder
    $this->travel(-5)->hours();

    // Ir a un instante concreto
    $this->travelTo(now()->subHours(6));

    // Volver a la hora actual
    $this->travelBack();
});
public function test_time_can_be_manipulated(): void
{
    // Avanzar
    $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();

    // Retroceder
    $this->travel(-5)->hours();

    // Ir a un instante concreto
    $this->travelTo(now()->subHours(6));

    // Volver a la hora actual
    $this->travelBack();
}

Viaje temporal con closures

Si pasas una closure a los métodos de viaje, el tiempo queda congelado en el instante indicado durante su ejecución y, al terminar, vuelve al momento original.
$this->travel(5)->days(function () {
    // Probar el estado dentro de 5 días
});

$this->travelTo(now()->subDays(10), function () {
    // Probar en un instante concreto
});

freezeTime(): detener el tiempo

freezeTime() fija la hora actual. freezeSecond() fija la hora al inicio del segundo actual.
use Illuminate\Support\Carbon;

// Congelar el tiempo, ejecutar la closure y reanudar
$this->freezeTime(function (Carbon $time) {
    // ...
});

// Congelar al inicio del segundo actual y ejecutar la closure
$this->freezeSecond(function (Carbon $time) {
    // ...
});

Ejemplo práctico: bloquear hilos inactivos

Manipular el tiempo es útil, por ejemplo, para probar funcionalidades como el bloqueo de un hilo de foro tras un periodo de inactividad.
use App\Models\Thread;

test('los hilos del foro se bloquean tras una semana de inactividad', function () {
    $thread = Thread::factory()->create();

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

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

public function test_forum_threads_lock_after_one_week_of_inactivity(): void
{
    $thread = Thread::factory()->create();

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

    $this->assertTrue($thread->isLockedByInactivity());
}
Los métodos como travel() solo pueden usarse en feature tests (clases que heredan de Tests\TestCase). No están disponibles en el TestCase base de PHPUnit.

Lista de métodos

Relacionados con mocks

MétodoDescripción
$this->mock(Class::class, fn)Crea un mock completo de la clase y lo registra en el contenedor
$this->partialMock(Class::class, fn)Mockea solo algunos métodos
$this->spy(Class::class)Crea un spy y lo registra en el contenedor
$this->instance(Class::class, $mock)Registra una instancia mock arbitraria en el contenedor
Facade::expects('method')Mockea un método de un facade
Facade::spy()Espía un facade
$spy->shouldHaveReceived('method')Verifica que el spy recibió una llamada

Manipulación del tiempo

MétodoDescripción
$this->travel(n)->unit()Avanza el tiempo la cantidad indicada
$this->travelTo(Carbon)Va a un instante concreto
$this->travelBack()Vuelve al momento actual
$this->freezeTime(fn)Congela el tiempo mientras se ejecuta la closure
$this->freezeSecond(fn)Congela el tiempo al inicio del segundo actual mientras se ejecuta la closure
Última modificación el 13 de julio de 2026