# Tester un module e-learning : la progression ne doit jamais reculer

> Une coupure réseau au mauvais moment, et la progression d'un apprenant repart de zéro. Voici comment écrire des tests qui rejouent ce scénario précis.

- Auteur : WordPress Développement
- Publié le : 2021-06-09
- Mis à jour le : 2021-06-09
- Catégorie : Tests
- URL : https://www.wpmoderne.fr/tests/tester-elearning-progression-jamais-reculer/

## L’essentiel

- La progression doit être monotone : elle ne redescend jamais spontanément
- Simuler une coupure réseau en cours d'écriture révèle les vraies failles
- Le test doit rejouer l'échec, pas seulement le cas nominal

« Perdu » 40 % de sa progression sur un module de prévention des risques qu'il avait pourtant terminé la veille : c'est ce dont s'est plaint un apprenant, après qu'une requête AJAX enregistrant l'avancement d'un module e-learning a été coupée à mi-chemin. Cette plainte, remontée par un client du secteur de la formation professionnelle, a posé une question précise à traiter par les tests.

Le module reposait sur une extension maison enregistrant la progression sous forme d'un pourcentage stocké en métadonnée utilisateur, mis à jour à chaque changement de leçon. Le bug ne venait pas d'une erreur de calcul, mais d'une écriture concurrente : une requête tardive, envoyée juste avant la coupure réseau côté client, était arrivée après une requête plus récente et avait écrasé la valeur la plus haute avec une valeur plus ancienne.

## Étape 1 : écrire le test qui décrit la règle

Avant de corriger quoi que ce soit, il fallait un test qui exprime clairement la règle attendue : la progression enregistrée pour un utilisateur ne doit jamais diminuer, quel que soit l'ordre d'arrivée des requêtes.

```
class ProgressionMonotoneTest extends WP_UnitTestCase
{
    public function test_une_requete_en_retard_ne_fait_pas_reculer_la_progression(): void
    {
        $user_id = self::factory()->user->create();

        $service = new ProgressionService();
        $service->enregistrer($user_id, 'module-42', 80);

        // Requête en retard, arrivée après une progression plus récente
        $service->enregistrer($user_id, 'module-42', 45);

        $this->assertSame(
            80,
            $service->lire($user_id, 'module-42'),
            'Une requête en retard ne doit jamais écraser une progression plus avancée.'
        );
    }
}
```

## Étape 2 : simuler la coupure réseau côté serveur

Le vrai piège n'était pas dans l'ordre logique des appels, mais dans le comportement du navigateur : un utilisateur qui ferme un onglet en cours de sauvegarde peut envoyer une requête qui arrive partiellement, ou dont la réponse n'est jamais reçue côté client, qui la renvoie alors une seconde fois. Le test suivant simule ce rejeu.

> L'essentiel à retenir : La progression doit être monotone : elle ne redescend jamais spontanément ; Simuler une coupure réseau en cours d'écriture révèle les vraies failles ; Le test doit rejouer l'échec, pas seulement le cas nominal

```
public function test_un_rejeu_apres_coupure_reste_sans_effet(): void
{
    $user_id = self::factory()->user->create();
    $service = new ProgressionService();

    $service->enregistrer($user_id, 'module-42', 100);

    // Le navigateur rejoue la même requête après une coupure, sans savoir
    // qu'elle avait déjà abouti côté serveur.
    $service->enregistrer($user_id, 'module-42', 100);
    $service->enregistrer($user_id, 'module-42', 60); // requête plus ancienne, arrivée en retard

    $this->assertSame(100, $service->lire($user_id, 'module-42'));
}
```

### Étape 3 : la correction, un simple mais crucial comparateur

Le correctif tient en quelques lignes, mais il fallait d'abord le test pour oser le poser sans crainte de régression :

```
public function enregistrer(int $user_id, string $module, int $pourcentage): void
{
    $actuel = (int) get_user_meta($user_id, "progression_{$module}", true);

    if ($pourcentage > $actuel) {
        update_user_meta($user_id, "progression_{$module}", $pourcentage);
    }
}
```

Rien de spectaculaire, mais sans les tests qui posent explicitement la règle de monotonie, ce genre de correctif a tendance à être « oublié » lors d'un futur refactoring qui repasserait par une écriture inconditionnelle.

## Étape 4 : couvrir les cas limites propres au secteur

- Un apprenant qui reprend un module sur un autre appareil : deux sessions concurrentes ne doivent pas produire de conflit visible.
- Une progression à 100 % suivie d'une reprise du module en mode révision, qui ne doit pas repasser sous 100 % dans les tableaux de suivi RH.
- Un module supprimé puis republié avec le même identifiant, qui ne doit pas hériter d'anciennes progressions orphelines.

> Un test de progression qui ne couvre que le cas nominal ne teste rien du tout : la vraie valeur du test est dans le scénario de coupure, qui est justement celui qu'on n'imagine pas en écrivant le code.

## Pour aller plus loin

Cette classe de bugs — une écriture tardive qui écrase un état plus récent — dépasse largement l'e-learning : on la retrouve dans tout système qui enregistre une progression, un score ou un état cumulatif à partir de requêtes asynchrones. La leçon générale est la même partout : dès qu'un état doit être monotone, le test doit explicitement rejouer le désordre, pas seulement l'ordre attendu.
