# Geler le temps dans vos tests avec Carbon ou uopz plutôt que sleep()

> Un test qui appelle sleep() pour attendre une expiration ralentit toute la suite. Contrôler précisément l'heure vue par le code testé règle le problème sans perdre une seconde.

- Auteur : Clément Hadrot
- Publié le : 2022-12-04
- Mis à jour le : 2022-12-04
- Catégorie : Tests
- URL : https://wpmoderne.dev.wordpress-developpement.fr/tests/geler-le-temps-tests-carbon-uopz/

## L’essentiel

- sleep multiplie le temps d'exécution sans rien prouver de plus
- uopz permute la fonction time native sans toucher au code testé
- Carbon offre une API de gel du temps plus lisible côté application

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'essentiel à retenir : sleep multiplie le temps d'exécution sans rien prouver de plus ; uopz permute la fonction time native sans toucher au code testé ; Carbon offre une API de gel du temps plus lisible côté application

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.
