Pourquoi mocker
Lors de la rédaction de tests, certains traitements ne doivent pas être exécutés (envoi de mail, cache, API externe).
On remplace ces traitements par des « imitations » : c’est le mock.
Avantages :
- Rapidité — pas d’appel externe ni de traitement lourd
- Stabilité — indépendance vis-à-vis de l’état des services externes
- Portée claire — chaque test cible une classe/méthode
Laravel expose des helpers pour mocker événements, jobs, façades, etc. En interne, il utilise Mockery, utilisable sans setup complexe.
Mocker des objets
Pour mocker un objet injecté via le conteneur, enregistrez l’instance de mock dans le conteneur.
Il retournera alors le mock au lieu d’instancier la classe.
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éthode mock()
Équivalent plus concis via la méthode de la classe de test de base.
use App\Service;
use Mockery\MockInterface;
$mock = $this->mock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
});
Méthode partialMock()
Pour ne mocker que certaines méthodes ; les autres s’exécutent normalement.
use App\Service;
use Mockery\MockInterface;
$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
$mock->expects('process');
});
Méthode spy()
Un spy ressemble à un mock mais on vérifie les interactions après l’exécution.
use App\Service;
$spy = $this->spy(Service::class);
// ... exécute le code testé ...
$spy->shouldHaveReceived('process');
Mocker une façade
Les façades, contrairement aux méthodes statiques classiques, sont mockables.
Vous gardez la testabilité de l’injection de dépendances avec une syntaxe concise.
Contrôleur utilisant le cache :
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* Retourne la liste des utilisateurs.
*/
public function index(): array
{
$value = Cache::get('key');
return [
// ...
];
}
}
Utilisez expects() pour mocker Cache::get.
<?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');
// ...
}
}
Ne mockez pas la façade Request : passez les valeurs via les méthodes HTTP de test (get, post, …). De même, appelez Config::set() dans le test plutôt que de mocker la façade Config.
Espionner une façade
Utilisez spy() sur la façade.
<?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);
}
Manipulation du temps
Pour tester une logique dépendante du temps, changez ce que now() ou Carbon::now() retourne.
La classe de base des tests fonctionnels fournit des helpers.
travel() — Déplacer le temps
test('time can be manipulated', function () {
// Avancer
$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();
// Reculer
$this->travel(-5)->hours();
// À un instant précis
$this->travelTo(now()->subHours(6));
// Retour au présent
$this->travelBack();
});
public function test_time_can_be_manipulated(): void
{
// Avancer
$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();
// Reculer
$this->travel(-5)->hours();
// À un instant précis
$this->travelTo(now()->subHours(6));
// Retour au présent
$this->travelBack();
}
Avec une closure
Passez une closure pour tester dans un contexte temporel donné puis revenir automatiquement.
$this->travel(5)->days(function () {
// État dans 5 jours
});
$this->travelTo(now()->subDays(10), function () {
// À un instant précis
});
freezeTime() — Figer le temps
freezeTime() fige l’heure actuelle ; freezeSecond() la fige au début de la seconde.
use Illuminate\Support\Carbon;
$this->freezeTime(function (Carbon $time) {
// ...
});
$this->freezeSecond(function (Carbon $time) {
// ...
});
Exemple : verrouillage des threads inactifs
Utile pour tester une fonction où un thread se verrouille après une période d’inactivité.
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());
}
Les méthodes de time travel sont disponibles dans les tests fonctionnels (héritant de Tests\TestCase), pas dans le TestCase de PHPUnit de base.
Récapitulatif
Mock
| Méthode | Description |
|---|
$this->mock(Class::class, fn) | Mock complet enregistré dans le conteneur |
$this->partialMock(Class::class, fn) | Mock partiel |
$this->spy(Class::class) | Spy enregistré dans le conteneur |
$this->instance(Class::class, $mock) | Enregistre une instance arbitraire |
Facade::expects('method') | Mocke une méthode de façade |
Facade::spy() | Espionne une façade |
$spy->shouldHaveReceived('method') | Vérifie sur un spy |
Temps
| Méthode | Description |
|---|
$this->travel(n)->unit() | Avance / recule dans le temps |
$this->travelTo(Carbon) | Aller à un instant précis |
$this->travelBack() | Retour au présent |
$this->freezeTime(fn) | Fige le temps pendant l’exécution |
$this->freezeSecond(fn) | Fige au début de la seconde |