Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1317 minAvancé

Tester et déboguer une procédure stockée ou un trigger

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire un script de test rejouable pour une procédure ou un trigger ;
  • trouver un bogue avec des traces et GET DIAGNOSTICS ;
  • versionner tes routines dans Git et les déployer par script ;
  • sauvegarder routines, triggers et événements avec mysqldump.

3 exercices corrigés

Tester une procédure stockée, c'est l'appeler avec des cas choisis à l'avance (le cas normal, les cas limites, les cas d'erreur) et vérifier automatiquement le résultat, dans un script que tu peux rejouer après chaque modification. MySQL n'a ni débogueur pas à pas ni outil de test intégré pour le SQL procédural : il faut construire les tiens, avec quelques procédures et un peu de méthode. Ce chapitre montre comment, puis comment versionner et sauvegarder tes routines.

Une procédure d'assertion

Le cœur d'un test automatique est l'assertion : une vérification qui échoue bruyamment si le résultat n'est pas celui attendu. Une petite procédure suffit :

sql
DELIMITER //
CREATE PROCEDURE verifier(IN p_condition BOOLEAN, IN p_message VARCHAR(100))
BEGIN
  IF p_condition IS NULL OR NOT p_condition THEN
    SET @echec = CONCAT('ÉCHEC : ', p_message);
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = @echec;
  END IF;
END //
DELIMITER ;

CALL verifier(1 = 2, 'démonstration d''un échec');
-- ERROR 1644 (45000): ÉCHEC : démonstration d'un échec

Une condition NULL est traitée comme un échec : un test qui ne sait pas répondre ne doit pas passer.

Un script de test pour une procédure

Le test de installer_logiciel (chapitre 7) couvre les quatre cas qui comptent : la dernière licence acceptée, la suivante refusée, un logiciel libre sans limite, un doublon refusé. Pour vérifier qu'une erreur est bien levée, on la rattrape dans un sous-bloc et on contrôle son numéro :

sql
DELIMITER //
CREATE PROCEDURE test_installer_logiciel()
BEGIN
  DECLARE v_nb  INT;
  DECLARE v_num INT DEFAULT 0;

  -- Cas 1 : la dernière licence Odoo est acceptée
  CALL installer_logiciel(9, 5);
  SELECT COUNT(*) INTO v_nb FROM installation WHERE id_logiciel = 5;
  CALL verifier(v_nb = 8, 'cas 1, 8 installations d''Odoo attendues');

  -- Cas 2 : la suivante est refusée par SIGNAL (erreur 1644), et rien n'est ajouté
  BEGIN
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
      GET DIAGNOSTICS CONDITION 1 v_num = MYSQL_ERRNO;
    CALL installer_logiciel(14, 5);
  END;
  CALL verifier(v_num = 1644, 'cas 2, erreur 1644 attendue');
  SELECT COUNT(*) INTO v_nb FROM installation WHERE id_logiciel = 5;
  CALL verifier(v_nb = 8, 'cas 2, toujours 8 installations d''Odoo');

  -- Cas 3 : un logiciel libre n'est pas limité
  CALL installer_logiciel(14, 3);
  CALL verifier(EXISTS (SELECT 1 FROM installation WHERE id_poste = 14 AND id_logiciel = 3),
                'cas 3, Firefox installé sur le poste 14');

  -- Cas 4 : un doublon est refusé (erreur 1062)
  SET v_num = 0;
  BEGIN
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
      GET DIAGNOSTICS CONDITION 1 v_num = MYSQL_ERRNO;
    CALL installer_logiciel(14, 3);
  END;
  CALL verifier(v_num = 1062, 'cas 4, erreur 1062 attendue');

  SELECT 'installer_logiciel : 4 cas réussis' AS resultat;
END //
DELIMITER ;

CALL test_installer_logiciel();
resultat
installer_logiciel : 4 cas réussis

Trois règles rendent ce test fiable :

  • Partir d'un état connu. Le test suppose la base fraîche : 7 installations d'Odoo. Lance-le après avoir rejoué parc.sql et parc-complements.sql, sur une base de test, jamais sur la base de production. Comme installer_logiciel valide sa propre transaction, le test laisse des traces : on remet la base à zéro avant chaque passage.
  • Tester les erreurs autant que les réussites. Le cas 2 vérifie à la fois l'erreur et l'absence d'effet : une procédure qui lève la bonne erreur mais a déjà inséré la ligne est fausse.
  • Un message par vérification : quand un test échoue, ÉCHEC : cas 2, erreur 1644 attendue dit tout de suite où chercher.

Tester un trigger sans rien laisser

Un trigger ne valide jamais de transaction lui-même : on peut donc le tester dans une transaction annulée à la fin. La base revient exactement à son état de départ, et le test se rejoue à volonté.

sql
START TRANSACTION;

UPDATE ticket SET statut = 'resolu' WHERE id_ticket = 12;
CALL verifier((SELECT date_cloture IS NOT NULL FROM ticket WHERE id_ticket = 12),
              'trg_ticket_cloture : date de clôture renseignée');

UPDATE ticket SET statut = 'en_cours' WHERE id_ticket = 9;
CALL verifier((SELECT date_cloture IS NULL FROM ticket WHERE id_ticket = 9),
              'trg_ticket_cloture : date effacée à la réouverture');

SELECT 'trg_ticket_cloture : 2 cas réussis' AS resultat;
ROLLBACK;

Après le ROLLBACK, le ticket 12 est toujours ouvert et le ticket 9 toujours resolu, avec sa date de clôture du 6 octobre. Si une vérification échoue, le script s'arrête sur l'erreur : pense à exécuter ROLLBACK à la main.

Déboguer : savoir ce qui se passe dedans

Quand une procédure donne un résultat faux sans erreur, il faut voir les valeurs intermédiaires. Trois techniques :

1. Un SELECT de débogage, retiré ensuite. Dans le client mysql, chaque SELECT sans INTO s'affiche : SELECT v_minutes, v_licences AS debug; au milieu de la procédure montre les valeurs au passage. Pratique, mais interdit dans une fonction et dans un trigger, et gênant pour une application qui ne s'attend pas à un jeu de résultats de plus.

2. Une table de traces, qui fonctionne partout, y compris dans les triggers et les événements :

sql
CREATE TABLE trace_debug (
  n       INT AUTO_INCREMENT PRIMARY KEY,
  quand   DATETIME(6) DEFAULT NOW(6),
  message VARCHAR(255)
);

-- dans la procédure à déboguer
INSERT INTO trace_debug (message)
VALUES (CONCAT('classer(', p_id_ticket, ') : v_minutes = ', COALESCE(v_minutes, 'NULL')));

Sur une procédure qui calcule SUM(duree_min) sans COALESCE, la trace v_minutes = NULL pour le ticket 7 explique tout de suite pourquoi le test v_minutes > 60 ne se comporte pas comme prévu. Attention : dans une transaction annulée, les traces disparaissent avec le reste.

3. Les avertissements. Après un CALL, SHOW WARNINGS; affiche les avertissements de la dernière instruction exécutée dans la procédure, par exemple l'avertissement 1329 d'un SELECT … INTO qui n'a rien trouvé, quand c'est la dernière instruction. Une instruction suivante efface la liste : c'est donc surtout utile pour tester un morceau de code isolé.

Quand la trace ne suffit pas, sors la requête de la procédure : remplace les paramètres par des valeurs, exécute-la seule dans le client, et regarde son plan avec EXPLAIN si elle est lente.

Les bogues classiques, à vérifier d'abord

SymptômeCause probableChapitre
Une requête de la procédure concerne toute la tableUn paramètre qui porte le nom d'une colonne2
Une variable garde une ancienne valeur, ou reste NULLUn SELECT … INTO qui n'a trouvé aucune ligne3
Une condition ne se déclenche jamaisUne comparaison avec NULL (= NULL, <> sur une colonne NULL)9
Une boucle de curseur s'arrête trop tôtUn SELECT … INTO qui déclenche le gestionnaire NOT FOUND5
La procédure « réussit » mais rien n'est faitUn gestionnaire EXIT ou CONTINUE sans RESIGNAL6
Les modifications de l'appelant sont validées malgré son ROLLBACKUn START TRANSACTION dans la procédure appelée7
Les accents des messages sont fauxLa routine a été créée avec un client en latin11
La procédure marche avec root, pas avec l'applicationUn droit manquant au propriétaire ou à l'appelant11

Pense aussi à vérifier ce que le serveur a réellement enregistré : SHOW CREATE PROCEDURE donne le code, mais aussi le sql_mode et le jeu de caractères en vigueur à la création. Une routine s'exécute toujours avec le sql_mode de sa création, même si celui du serveur a changé depuis : une procédure créée par un outil qui désactive le mode strict continuera de tronquer silencieusement les valeurs trop longues.

Versionner et déployer tes routines

Une procédure modifiée directement sur le serveur, dans phpMyAdmin, n'existe dans aucun dépôt : impossible de savoir qui l'a changée, ni de revenir en arrière. Traite tes routines comme du code :

  • un fichier par routine (routines/cloturer_ticket.sql), ou un fichier de migration par évolution (migrations/2026-11-installer-logiciel.sql), versionné dans Git avec le reste de l'application (cours Git et GitHub) ;
  • chaque fichier est rejouable : DROP PROCEDURE IF EXISTS, puis CREATE PROCEDURE, avec les DELIMITER ;
  • le déploiement se fait par script, jamais à la main : mysql --default-character-set=utf8mb4 -u admin_parc -p parc < routines/cloturer_ticket.sql ;
  • chaque migration a son script d'annulation (DROP TRIGGER …, DROP PROCEDURE …) : c'est ce que recommande la méthode de l'épreuve E6 quand tu ajoutes un trigger à ta réalisation.
sql
-- routines/cloturer_ticket.sql : rejouable autant de fois qu'on veut
DROP PROCEDURE IF EXISTS cloturer_ticket;
DELIMITER //
CREATE PROCEDURE cloturer_ticket(…)
COMMENT 'Gère sa propre transaction. Erreurs 1644 : ticket inconnu ou déjà résolu.'
BEGIN
  …
END //
DELIMITER ;
GRANT EXECUTE ON PROCEDURE parc.cloturer_ticket TO 'helpdesk'@'192.168.21.20';

N'oublie pas le GRANT à la fin : supprimer une procédure supprime aussi les droits EXECUTE donnés dessus.

Sauvegarder les routines avec mysqldump

mysqldump sauvegarde les triggers par défaut, mais ni les procédures et fonctions, ni les événements :

bash
# Tout : tables, données, triggers, routines et événements
mysqldump --default-character-set=utf8mb4 --routines --events -u sauvegarde -p parc > parc-complet.sql

# Seulement le code : routines, événements et triggers, sans tables ni données
mysqldump --no-data --no-create-info --routines --events -u sauvegarde -p parc > parc-code.sql

Une sauvegarde sans --routines --events se restaure sans erreur, et l'application ne s'en aperçoit qu'au premier CALL : ERROR 1305 (42000): PROCEDURE parc.cloturer_ticket does not exist. Teste tes restaurations (restaurer et tester la restauration), et vérifie que le compte de sauvegarde a bien les privilèges nécessaires (SHOW VIEW, TRIGGER, EVENT et la lecture des routines).

Exercices

Exercice 1 sur 3 Tester affecter_poste Réussi À revoir

Écris une procédure test_affecter_poste() qui teste la procédure affecter_poste du chapitre 6 sur trois cas : un poste en stock affecté avec succès, un poste déjà affecté à quelqu'un d'autre refusé, un poste inconnu refusé. Le test doit laisser la base dans son état de départ.

Voir la réponseMasquer la réponse de l'exercice 1

affecter_poste n'ouvre pas de transaction : on peut tout faire dans une transaction annulée à la fin.

sql
DELIMITER //
CREATE PROCEDURE test_affecter_poste()
BEGIN
  DECLARE v_num INT DEFAULT 0;

  START TRANSACTION;

  -- Cas 1 : le PC-0014, en stock, est affecté à Paul Martin
  CALL affecter_poste(14, 14);
  CALL verifier((SELECT id_employe FROM poste WHERE id_poste = 14) = 14, 'cas 1, poste 14 affecté');

  -- Cas 2 : le PC-0004 appartient déjà à Sofiane Benali
  BEGIN
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION GET DIAGNOSTICS CONDITION 1 v_num = MYSQL_ERRNO;
    CALL affecter_poste(4, 14);
  END;
  CALL verifier(v_num = 1644, 'cas 2, refus attendu');
  CALL verifier((SELECT id_employe FROM poste WHERE id_poste = 4) = 4, 'cas 2, poste 4 inchangé');

  -- Cas 3 : poste inconnu
  SET v_num = 0;
  BEGIN
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION GET DIAGNOSTICS CONDITION 1 v_num = MYSQL_ERRNO;
    CALL affecter_poste(99, 14);
  END;
  CALL verifier(v_num = 1644, 'cas 3, refus attendu');

  ROLLBACK;
  SELECT 'affecter_poste : 3 cas réussis' AS resultat;
END //
DELIMITER ;

Une limite : si une assertion échoue, l'erreur interrompt la procédure avant le ROLLBACK, et la transaction reste ouverte. Ajoute un gestionnaire EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; au début du test pour annuler aussi dans ce cas.

Tu avais trouvé ?

Exercice 2 sur 3 Diagnostiquer sans voir le code Réussi À revoir

Pour chaque symptôme, donne la cause la plus probable et ce que tu vérifies en premier.

  1. Après la restauration de la sauvegarde de cette nuit, l'application affiche « PROCEDURE parc.cloturer_ticket does not exist ».
  2. Un trigger d'historique fonctionne quand un poste change d'utilisateur, mais n'écrit rien quand un poste en stock est affecté pour la première fois.
  3. Depuis l'application, l'appel de cloturer_ticket renvoie « execute command denied to user 'routines_parc'@'localhost' ».
  4. Dans l'application, la plupart des messages d'erreur de la base s'affichent correctement, mais ceux d'une procédure récente montrent « Ã© » à la place des « é ».
Voir la réponseMasquer la réponse de l'exercice 2
  1. La sauvegarde a été faite sans --routines : les tables et les données sont revenues, pas les procédures. Vérifie la commande de sauvegarde, recrée les routines depuis le dépôt Git, et ajoute --routines --events au script.
  2. La comparaison OLD.id_employe <> NEW.id_employe vaut NULL quand l'ancienne valeur est NULL (poste en stock). Remplace-la par NOT (OLD.id_employe <=> NEW.id_employe).
  3. Le propriétaire (DEFINER) de la procédure n'a pas le droit EXECUTE sur elle. GRANT EXECUTE ON PROCEDURE parc.cloturer_ticket TO 'routines_parc'@'localhost';.
  4. La routine qui produit le message mal accentué a été créée depuis un client en latin1. Vérifie la colonne character_set_client de SHOW CREATE PROCEDURE (ou de SHOW TRIGGERS), puis recrée-la avec --default-character-set=utf8mb4.

Tu avais trouvé ?

Exercice 3 sur 3 Un script de déploiement Réussi À revoir

Écris le fichier de migration qui ajoute à la base de production le trigger trg_intervention_technicien du chapitre 8, au nom du propriétaire routines_parc, ainsi que son script d'annulation. Quels droits faut-il vérifier avant de déployer ?

Voir la réponseMasquer la réponse de l'exercice 3
sql
-- migrations/2026-11-trigger-technicien.sql
DROP TRIGGER IF EXISTS trg_intervention_technicien;
DELIMITER //
CREATE DEFINER = 'routines_parc'@'localhost' TRIGGER trg_intervention_technicien
BEFORE INSERT ON intervention
FOR EACH ROW
BEGIN
  IF NOT EXISTS (SELECT 1
                 FROM employe e
                 JOIN service s ON s.id_service = e.id_service
                 WHERE e.id_employe = NEW.id_technicien AND s.code = 'INFO') THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Seul un membre du service informatique peut intervenir';
  END IF;
END //
DELIMITER ;

-- migrations/2026-11-trigger-technicien.annulation.sql
DROP TRIGGER IF EXISTS trg_intervention_technicien;

Avant le déploiement : le compte qui exécute le script a TRIGGER sur intervention et SET_ANY_DEFINER (il désigne un autre propriétaire) ; le propriétaire routines_parc a TRIGGER sur intervention et SELECT sur employe et service, sinon toute insertion d'intervention échouera. Puis le test : une insertion par un technicien acceptée, une par un autre salarié refusée, avec le compte de l'application.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : PostgreSQL (PL/pgSQL) et SQL Server (T-SQL) : les équivalencesRevoir le sommaire du cours