# Un test qui échoue deux nuits par an : le piège du changement d’heure

> Deux fois par an, une suite de tests qui manipule des créneaux horaires échoue sans qu'aucun code n'ait changé. Le coupable : le passage à l'heure d'été ou d'hiver.

- Auteur : WordPress Développement
- Publié le : 2022-10-02
- Mis à jour le : 2022-10-02
- Catégorie : Tests
- URL : https://www.wpmoderne.fr/tests/test-echoue-deux-nuits-an-piege-changement-heure/

## L’essentiel

- Le changement d'heure crée deux nuits de 23 ou 25 heures par an
- Un calcul de durée en heure locale peut se tromper d'une heure
- Calculer en UTC, afficher en heure locale : le biais disparaît

Dans la nuit du dernier dimanche de mars, une horloge réglée sur l'heure de Paris saute de 2 h 00 à 3 h 00 : cette nuit-là ne compte que vingt-trois heures. Dans celle du dernier dimanche d'octobre, le mouvement inverse se produit, et la nuit en compte vingt-cinq. Le reste de l'année, personne n'y pense. Ces deux nuits-là, en revanche, un calcul de durée qui ignore ce détail produit un résultat faux, exactement une heure d'écart, sans qu'aucune ligne de code n'ait été modifiée entre-temps.

Ce type d'échec est particulièrement déroutant à diagnostiquer, car il ne se manifeste que deux fois par an, sur un créneau très étroit, et disparaît spontanément le reste du temps. Une équipe qui n'a jamais rencontré ce cas précis cherche d'abord du côté d'une régression de code, d'une dépendance mise à jour ou d'un problème d'environnement, avant de songer au calendrier lui-même.

## Le mécanisme du changement d'heure, deux fois par an

En France comme dans le reste de l'Union européenne, l'heure légale change deux fois par an : avance d'une heure fin mars, retour en arrière fin octobre. PHP applique ce décalage automatiquement dès qu'un objet de date est associé au fuseau horaire `Europe/Paris`, à condition que la base de données de fuseaux horaires du système soit à jour. Le calcul devient délicat uniquement quand du code additionne ou soustrait des durées exprimées en heures locales à cheval sur l'une de ces deux nuits particulières.

Le reste de l'année, une heure locale et une durée en heures se comportent de façon parfaitement prévisible : soixante minutes valent toujours une heure, un jour compte toujours vingt-quatre heures. Ces deux nuits font exception, et un test qui ne couvre jamais ce cas précis peut vivre des années sans jamais révéler le problème.

## Le test qui tombe précisément ces nuits-là

```
function calcule_duree_creneau_en_heures( DateTimeImmutable $debut, DateTimeImmutable $fin ): int {
    return $fin->diff( $debut )->h;
}

// Un créneau de réservation qui chevauche le passage à l'heure d'hiver
$debut = new DateTimeImmutable( '2022-10-29 22:00:00', new DateTimeZone( 'Europe/Paris' ) );
$fin   = new DateTimeImmutable( '2022-10-30 04:00:00', new DateTimeZone( 'Europe/Paris' ) );

// Six heures d'écart en apparence, mais la nuit compte une heure de plus
$duree = calcule_duree_creneau_en_heures( $fin, $debut );
```

Sur toute autre nuit de l'année, cet appel renverrait bien six. Cette nuit-là précisément, il renvoie sept, puisque l'horloge recule d'une heure entre les deux instants et que `DateTimeImmutable` tient compte du décalage réel appliqué au fuseau horaire. Un test qui fige la date attendue à six, sans jamais avoir été rejoué sur une nuit de changement d'heure, réussit pendant des mois avant d'échouer soudainement, une seule fois, sans explication apparente dans les journaux de la suite.

> L'essentiel à retenir : Le changement d'heure crée deux nuits de 23 ou 25 heures par an ; Un calcul de durée en heure locale peut se tromper d'une heure ; Calculer en UTC, afficher en heure locale : le biais disparaît

## Diagnostiquer : confirmer que c'est bien le changement d'heure

Avant de corriger, il est utile de vérifier l'hypothèse plutôt que de la supposer. Deux vérifications rapides permettent de la confirmer :

- Comparer la date exacte de l'échec constaté avec le calendrier officiel des changements d'heure de l'année concernée : dernier dimanche de mars, dernier dimanche d'octobre.
- Rejouer le test suspecté en fixant artificiellement les deux dates de la nuit incriminée, pour vérifier que l'écart d'une heure se reproduit à volonté, indépendamment de tout autre facteur.

```
public function test_duree_creneau_nuit_hiver(): void {
    $debut = new DateTimeImmutable( '2022-10-29 22:00:00', new DateTimeZone( 'Europe/Paris' ) );
    $fin   = new DateTimeImmutable( '2022-10-30 04:00:00', new DateTimeZone( 'Europe/Paris' ) );

    $duree = calcule_duree_creneau_en_heures( $fin, $debut );

    // Confirme le décalage : sept heures réelles, pas six
    $this->assertSame( 7, $duree );
}
```

## Le correctif : raisonner en UTC pour le calcul, en heure locale pour l'affichage

La correction la plus fiable consiste à séparer clairement deux besoins qui se confondent trop souvent dans le même calcul : mesurer une durée réelle, et afficher une heure lisible pour un visiteur. Le premier doit s'appuyer sur un fuseau horaire fixe qui ignore les changements d'heure, comme `UTC` ; le second seulement doit convertir vers `Europe/Paris` au moment de l'affichage.

```
function calcule_duree_creneau_en_heures( DateTimeImmutable $debut, DateTimeImmutable $fin ): int {
    $debut_utc = $debut->setTimezone( new DateTimeZone( 'UTC' ) );
    $fin_utc   = $fin->setTimezone( new DateTimeZone( 'UTC' ) );

    return $fin_utc->diff( $debut_utc )->h;
}
```

Une fois le calcul ramené en UTC, la durée obtenue correspond au nombre réel d'heures écoulées, quelle que soit la nuit concernée. L'affichage destiné à un visiteur, lui, continue de convertir la date vers `Europe/Paris` au moment de la présenter, sans influencer le calcul lui-même.

> Un calcul de durée ne devrait jamais dépendre d'un fuseau horaire qui change deux fois par an : seul l'affichage a besoin de l'heure locale, jamais l'arithmétique.

## Prévenir : ajouter ces deux nuits au calendrier de tests

Une fois ce cas corrigé sur une fonction précise, il reste utile de repérer les autres endroits du projet où une durée est calculée à partir de deux objets de date en heure locale. Une recherche du mot-clé `diff(` associée à un fuseau horaire autre qu'UTC dans le code métier permet de retrouver rapidement ces candidats. Ajouter, dans la suite de tests, un cas fixé sur la nuit de fin mars et un autre sur la nuit de fin octobre de l'année en cours garantit que ce biais reviendra à chaque exécution, plutôt que de réapparaître une seule fois par semestre sans prévenir.

## En résumé

Un test intermittent qui échoue uniquement deux nuits par an, toujours aux mêmes dates, révèle presque toujours un calcul de durée effectué en heure locale sur un fuseau horaire soumis au changement d'heure. Ramener ce calcul en UTC, et réserver la conversion en heure locale au seul affichage, élimine cette source d'erreur qui autrement ne se révèle que deux fois par an, au pire moment.
