Une suite de tests vérifiant l’expiration d’un jeton d’accès temporaire, valable quinze minutes, contenait un appel sleep(2) suivi d’un commentaire « approximation raisonnable, le jeton devrait être expiré dans un vrai scénario long ». En réalité, ce test ne vérifiait rien du tout : deux secondes ne suffisent jamais à faire expirer un jeton de quinze minutes, et le test passait uniquement parce que la logique d’expiration n’était jamais réellement exercée. Cumulés sur l’ensemble de la suite, les appels à sleep() disséminés dans divers fichiers ajoutaient 47 secondes d’attente pure, sans apporter la moindre garantie supplémentaire.
La solution ne consiste pas à augmenter la durée du sleep() jusqu’à dépasser quinze minutes — ce qui rendrait la suite inutilisable au quotidien — mais à contrôler directement l’heure que le code testé perçoit, sans jamais attendre réellement.
Option 1 — uopz pour remplacer time() au niveau du langage
L’extension PECL uopz permet de redéfinir temporairement le comportement d’une fonction native PHP, y compris time(), sans modifier une seule ligne du code testé :
public function test_jeton_expire_apres_quinze_minutes(): void {
$instant_creation = time();
$jeton = creer_jeton_acces(3600); // durée en secondes non pertinente ici, gérée par expiration
uopz_set_return('time', $instant_creation + (16 * MINUTE_IN_SECONDS));
$this->assertFalse(jeton_est_valide($jeton));
uopz_unset_return('time');
}
Le code de production n’a besoin d’aucune modification : il continue d’appeler time() normalement, uopz intercepte l’appel au niveau de l’extension. La contrepartie est une dépendance à une extension PECL qui doit être installée sur chaque environnement, y compris en CI.
Option 2 — Carbon avec une horloge injectée
Quand le projet utilise déjà Carbon (souvent le cas via des dépendances Composer plus larges), sa méthode Carbon::setTestNow() offre un contrôle similaire, sans dépendre d’une extension PECL, à condition que le code de production utilise Carbon plutôt que time() ou current_time() directement :
use Carbon\Carbon;
public function test_jeton_expire_apres_quinze_minutes(): void {
Carbon::setTestNow(Carbon::now());
$jeton = creer_jeton_acces();
Carbon::setTestNow(Carbon::now()->addMinutes(16));
$this->assertFalse(jeton_est_valide($jeton));
Carbon::setTestNow(); // réinitialise l'horloge réelle, indispensable
}

L’appel final à Carbon::setTestNow() sans argument est crucial : oublié, il laisse l’horloge figée pour tous les tests suivants de la suite, un piège classique qui reproduit exactement le problème de fuite d’état déjà traité par ailleurs, mais appliqué au temps plutôt qu’à une variable métier.
Comparer les deux approches
| Critère | uopz | Carbon |
|---|---|---|
| Modification du code de production requise | Non | Oui, si time() natif était utilisé auparavant |
| Dépendance supplémentaire | Extension PECL à installer | Paquet Composer, souvent déjà présent |
| Lisibilité de l’API | Technique, bas niveau | Expressive (addMinutes, subDays…) |
Un piège commun aux deux approches : oublier de restaurer l’horloge réelle
Que l’on utilise uopz_unset_return() ou Carbon::setTestNow() sans argument, la restauration de l’horloge réelle doit systématiquement figurer dans un tearDown(), jamais seulement en fin de méthode de test — un test qui échoue avant d’atteindre sa ligne de restauration laisse l’horloge figée pour tous les tests suivants :
protected function tearDown(): void {
Carbon::setTestNow();
if (function_exists('uopz_unset_return')) {
uopz_unset_return('time');
}
parent::tearDown();
}
Un
sleep()dans un test n’attend jamais assez longtemps pour prouver quoi que ce soit d’utile, et attend toujours trop longtemps pour rester agréable à exécuter au quotidien — le temps simulé résout les deux problèmes à la fois.
Ce sujet ne couvre pas
Cette technique de gel du temps s’applique à la logique métier qui raisonne sur des durées et des expirations. Elle est complémentaire, mais distincte, des tests de fréquence de cron déjà traités par ailleurs, qui portent spécifiquement sur la replanification d’événements récurrents plutôt que sur l’expiration d’un état ponctuel.
Variantes
Sur des projets plus anciens sans Carbon ni uopz disponibles, une solution de repli consiste à injecter une source d’horloge personnalisée sous forme d’interface simple, remplaçable par un stub en test — plus de code à écrire, mais aucune dépendance externe supplémentaire à installer sur les environnements de production.