Perché usare i mock
Quando scrivi test, ci sono operazioni che non vuoi eseguire realmente: invio email, letture/scritture in cache, chiamate ad API esterne.
Sostituire queste operazioni con “impostori che fingono di essere veri” si chiama mocking.
Vantaggi dei mock:
- Test veloci — non esegui servizi esterni od operazioni pesanti, quindi i test finiscono subito
- Test stabili — non dipendi dallo stato dei servizi esterni, il risultato è sempre lo stesso
- Test più mirati — verifichi una singola classe o metodo
Laravel fornisce helper pronti all’uso per mocking di eventi, job e facade.
Internamente usa Mockery; li usi senza configurazione complessa.
Oggetti mock
Per fare il mock di un oggetto iniettato tramite il service container, registra un’istanza mock nel container.
Il container userà l’istanza mock invece di crearne una reale.
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');
})
);
}
Metodo mock()
Con il metodo mock() fornito dalla classe base dei test di Laravel scrivi lo stesso in modo più conciso.
use App\Service;
use Mockery\MockInterface;
$mock = $this->mock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
});
Metodo partialMock()
Se vuoi mockare solo alcuni metodi di un oggetto, usa partialMock().
I metodi non mockati vengono eseguiti normalmente.
use App\Service;
use Mockery\MockInterface;
$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
});
Metodo spy()
Uno spy è simile a un mock, ma verifica le interazioni dopo l’esecuzione del codice.
Il mock imposta in anticipo “questo metodo dovrà essere chiamato”; lo spy verifica a posteriori “questo metodo è stato chiamato?”.
use App\Service;
$spy = $this->spy(Service::class);
// ...Esegui il codice sotto test...
$spy->shouldHaveReceived('process');
Mock delle facade
Le facade, diversamente dalle normali chiamate a metodi statici, possono essere mockate.
Hai la stessa testabilità della dependency injection con una sintassi concisa.
Considera ad esempio un controller che usa la cache.
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* Restituisce l'elenco di tutti gli utenti
*/
public function index(): array
{
$value = Cache::get('key');
return [
// ...
];
}
}
Per mockare il metodo get della facade Cache 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');
// ...
}
}
Non mockare la facade Request. Passa invece gli input ai metodi di test HTTP come get o post. Analogamente, invece di mockare la facade Config, chiama Config::set() nel test.
Spy delle facade
Per monitorare una facade con uno spy, chiama il metodo spy() sulla facade corrispondente.
Lo spy è utile quando vuoi verificare le interazioni dopo l’esecuzione del codice.
<?php
use Illuminate\Support\Facades\Cache;
test('values are stored in cache', 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);
}
Manipolazione del tempo
Nei test che verificano logiche dipendenti dal tempo è utile poter cambiare l’orario restituito da now() o Carbon::now().
La classe base dei test feature di Laravel fornisce helper per manipolare il tempo.
travel() — spostare il tempo
test('time can be manipulated', function () {
// Vai avanti nel tempo
$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();
// Torna indietro nel tempo
$this->travel(-5)->hours();
// Sposta a un istante specifico
$this->travelTo(now()->subHours(6));
// Torna all'orario corrente
$this->travelBack();
});
public function test_time_can_be_manipulated(): void
{
// Vai avanti nel tempo
$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();
// Torna indietro nel tempo
$this->travel(-5)->hours();
// Sposta a un istante specifico
$this->travelTo(now()->subHours(6));
// Torna all'orario corrente
$this->travelBack();
}
Spostamento del tempo con closure
Passando una closure ai metodi di travel, il tempo viene fissato all’istante indicato durante l’esecuzione della closure e ripristinato alla fine.
$this->travel(5)->days(function () {
// Testa lo stato dopo 5 giorni
});
$this->travelTo(now()->subDays(10), function () {
// Testa in un istante specifico
});
freezeTime() — fermare il tempo
freezeTime() congela l’orario corrente. freezeSecond() lo congela all’inizio del secondo corrente.
use Illuminate\Support\Carbon;
// Congela il tempo, esegue la closure e riprende
$this->freezeTime(function (Carbon $time) {
// ...
});
// Congela all'inizio del secondo corrente ed esegue la closure
$this->freezeSecond(function (Carbon $time) {
// ...
});
Esempio pratico: lock su thread inattivi
La manipolazione del tempo aiuta a testare funzionalità come i thread di un forum che si bloccano dopo un certo periodo di inattività.
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();
});
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());
}
I metodi di manipolazione del tempo come travel() sono utilizzabili solo nei feature test (classi che estendono Tests\TestCase). Non funzionano nel TestCase base di PHPUnit.
Elenco dei metodi
Legati ai mock
| Metodo | Descrizione |
|---|
$this->mock(Class::class, fn) | Crea un mock completo della classe e lo registra nel container |
$this->partialMock(Class::class, fn) | Mocka solo alcuni metodi |
$this->spy(Class::class) | Crea uno spy e lo registra nel container |
$this->instance(Class::class, $mock) | Registra un’istanza mock qualsiasi nel container |
Facade::expects('method') | Mocka un metodo della facade |
Facade::spy() | Monitora la facade con uno spy |
$spy->shouldHaveReceived('method') | Verifica che uno spy abbia ricevuto la chiamata |
Legati al tempo
| Metodo | Descrizione |
|---|
$this->travel(n)->unit() | Sposta il tempo dell’unità indicata |
$this->travelTo(Carbon) | Vai a un istante specifico |
$this->travelBack() | Torna all’orario corrente |
$this->freezeTime(fn) | Congela il tempo ed esegue la closure |
$this->freezeSecond(fn) | Congela all’inizio del secondo ed esegue la closure |