vendredi 25 septembre 2026

À propos

Contact

Tests

Simuler une base de données coupée en cours de requête pour tester la résilience

Provoquer volontairement une coupure de connexion pendant un test d'intégration pour vérifier qu'une extension échoue proprement plutôt que de corrompre des données.

Par Clément Hadrot • 15 août 2024 • 4 min de lecture • Aucun commentaire
Simuler une base de données coupée en cours de requête pour tester la résilience

Une extension de synchronisation de stock, qui écrivait sur trois tables successives à chaque mise à jour de commande, a un jour produit des commandes affichées comme « payées » côté client mais jamais décrémentées côté stock. La cause : une coupure réseau MySQL de quelques secondes, survenue exactement entre la deuxième et la troisième écriture de la séquence, un incident que personne ne pouvait reproduire à la demande et que les tests existants ne couvraient évidemment jamais.

Plutôt que d’attendre qu’un tel incident se reproduise en production, nous avons construit un test d’intégration qui provoque délibérément cette coupure au bon moment, pour vérifier que le code réagit en refusant proprement l’opération plutôt qu’en laissant une trace partielle.

Provoquer une coupure contrôlée avec un wrapper de connexion

Plutôt que de couper le réseau physiquement, ce qui serait ingérable dans une suite automatisée, on remplace temporairement l’objet $wpdb par une classe qui hérite de wpdb et simule une perte de connexion après un nombre donné de requêtes :

class WPDB_Coupure_Simulee extends wpdb {

    private $requetes_avant_coupure;
    private $compteur = 0;

    public function __construct( $requetes_avant_coupure ) {
        parent::__construct( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST );
        $this->requetes_avant_coupure = $requetes_avant_coupure;
    }

    public function query( $query ) {
        $this->compteur++;
        if ( $this->compteur > $this->requetes_avant_coupure ) {
            $this->last_error = 'MySQL server has gone away';
            return false;
        }
        return parent::query( $query );
    }
}

Écrire le test qui vérifie l’absence d’état partiel

L'essentiel à retenir : Couper la connexion au milieu d'une séquence d'écritures ; Vérifier qu'aucune donnée partielle ne subsiste ; Attraper l'exception plutôt que laisser une erreur fatale
class Test_Resilience_Synchronisation_Stock extends WP_UnitTestCase {

    public function test_coupure_apres_deuxieme_ecriture_ne_laisse_pas_etat_incoherent() {
        global $wpdb;
        $wpdb_original = $wpdb;
        $wpdb = new WPDB_Coupure_Simulee( 2 );

        $commande_id = self::factory()->post->create( [ 'post_type' => 'shop_order' ] );

        try {
            synchroniser_stock_commande( $commande_id );
            $this->fail( 'Une exception était attendue après la coupure simulée.' );
        } catch ( Synchronisation_Exception $e ) {
            $this->assertStringContainsString( 'gone away', $e->getMessage() );
        }

        $wpdb = $wpdb_original;

        $commande = wc_get_order( $commande_id );
        $this->assertEquals( 'pending', $commande->get_status() );
        $this->assertEmpty( get_post_meta( $commande_id, '_stock_decremente', true ) );
    }
}

Ce test échoue immédiatement contre le code d’origine de l’incident, puisque celui-ci ne vérifiait le résultat d’aucune des trois écritures : il continuait la séquence même après un false retourné par $wpdb->query(), laissant le statut de commande mis à jour sans la décrémentation de stock correspondante.

Le correctif : vérifier chaque écriture et savoir revenir en arrière

La correction consiste à englober la séquence dans une transaction explicite, avec vérification systématique du résultat de chaque requête avant de poursuivre :

function synchroniser_stock_commande( $commande_id ) {
    global $wpdb;
    $wpdb->query( 'START TRANSACTION' );

    $ok = $wpdb->query( $wpdb->prepare(
        "UPDATE {$wpdb->prefix}stock SET quantite = quantite - 1 WHERE produit_id = %d", $produit_id
    ) );

    if ( false === $ok ) {
        $wpdb->query( 'ROLLBACK' );
        throw new Synchronisation_Exception( $wpdb->last_error );
    }

    // suite de la séquence, avec la même vérification à chaque étape

    $wpdb->query( 'COMMIT' );
}

Toutes les tables concernées doivent utiliser le moteur InnoDB pour que START TRANSACTION ait un effet réel ; MyISAM, encore présent sur d’anciennes installations, ignore silencieusement les transactions, ce qui rendrait ce correctif inopérant sans qu’aucune erreur ne le signale.

Étendre le principe à d’autres points de coupure

  • Coupure avant la première écriture : le cas le plus simple, doit échouer immédiatement sans rien tenter.
  • Coupure entre deux écritures : le cas le plus dangereux, celui de l’incident réel.
  • Coupure juste après la dernière écriture mais avant la confirmation applicative : à vérifier séparément si le code envoie un accusé de réception externe (webhook, e-mail) après le commit.

En résumé

Une coupure de base de données en environnement réel est un incident rare mais pas exceptionnel, en particulier sur une infrastructure mutualisée ou lors d’une maintenance de l’hébergeur. Simuler ce cas en test coûte une classe de quelques lignes et permet de vérifier que le code échoue proprement plutôt que de corrompre silencieusement des données, ce qui est infiniment plus coûteux à réparer après coup qu’à prévenir en amont.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi