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 facadeConfig, llama al métodoConfig::seten 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();
});