Les tests de base de données offrent aux ingénieurs QA une troisième couche de vérification au-delà des tests UI et API. Des requêtes SQL directes confirment que les bonnes données ont atterri dans la bonne table avec les bonnes valeurs après toute action.
Pourquoi le SQL fait partie de l'outillage d'un ingénieur QA
Toute application web a la même structure : un frontend qui affiche des données, une API qui traite les requêtes, et une base de données qui stocke l'état. Les tests automatisés via l'UI vérifient la première couche. Les tests API vérifient la deuxième. Mais la troisième couche (si les bonnes données ont vraiment atterri dans la bonne table avec les bonnes valeurs) reste invisible à moins de regarder directement.
Ce manque se manifeste constamment en pratique. Une soumission de formulaire renvoie HTTP 200, l'UI affiche un message de succès, mais l'écriture en base a silencieusement échoué. Une violation de contrainte avalée par le gestionnaire d'erreurs en est souvent la cause. Une mise à jour de statut se reflète visuellement dans l'UI parce que le frontend a mis à jour son état local, mais l'appel API censé écrire dans le backend n'a jamais été lancé. Un script de seed s'exécute, le test tourne, et vous découvrez que les données de test ont été insérées dans la base de données du mauvais environnement.
Le SQL vous donne trois capacités concrètes : vérifier que les actions ont eu l'effet attendu sur les données, configurer des données de test plus rapidement que n'importe quel workflow UI, et déboguer des échecs que l'interface de l'application vous cache.
Rien de tout cela ne nécessite des connaissances avancées en bases de données. Cinq patterns de requêtes couvrent la grande majorité de ce qu'un ingénieur QA fait en pratique.
Les cinq requêtes qui couvrent 80% du travail QA
Ces cinq patterns gèrent la plupart des situations que vous rencontrerez en tant que testeur.
SELECT avec WHERE récupère les lignes qui correspondent à vos critères :-- Obtenir un utilisateur spécifique par email
SELECT id, email, role, is_active
FROM users
WHERE email = 'testuser@example.com';
-- Obtenir toutes les commandes d'un client
SELECT id, status, total_amount, created_at
FROM orders
WHERE customer_id = 42;
-- Obtenir les commandes passées dans les dernières 24 heures
SELECT id, customer_id, status, total_amount
FROM orders
WHERE created_at > NOW() - INTERVAL '24 hours';-- Commandes avec l'email du client
SELECT orders.id, orders.status, orders.total_amount, users.email
FROM orders
JOIN users ON orders.customer_id = users.id
WHERE orders.id = 1001;-- Combien de commandes par statut
SELECT status, COUNT(*) AS total
FROM orders
GROUP BY status;INSERT INTO users (email, password_hash, role, created_at)
VALUES ('qa_test_user@test.com', 'hashed_value', 'customer', NOW());UPDATE orders
SET status = 'cancelled'
WHERE id = 1001;UPDATE orders SET status = 'cancelled' sans filtre annule chaque commande de la table. Écrivez toujours la clause WHERE en premier, puis ajoutez l'action.Ces cinq patterns forment la base. Tout le reste dans cet article s'appuie sur eux.
Vérifier les résultats de tests : asserter ce que l'UI ne peut pas vous montrer
Après une action de test, la vérification la plus directe est une requête qui vérifie l'état attendu dans la base. Si la requête renvoie la ligne attendue, l'opération a réussi de bout en bout. Si elle ne renvoie rien, quelque chose a échoué entre l'action utilisateur et l'état persistant, même si l'UI semblait réussir.
Après l'inscription d'un utilisateur :
-- L'utilisateur a-t-il été créé avec les bonnes valeurs par défaut ?
SELECT id, email, role, is_active, email_verified
FROM users
WHERE email = 'newuser@test.com';Vous attendez une ligne, avec role = 'customer', is_active = true, et email_verified = false. Tout écart est un bug à investiguer avant d'atteindre la production.
Après avoir passé une commande :
-- La commande a-t-elle atterri avec le bon statut et montant ?
SELECT
o.id,
o.status,
o.total_amount,
o.created_at,
u.email AS customer_email
FROM orders o
JOIN users u ON o.customer_id = u.id
WHERE u.email = 'testuser@test.com'
ORDER BY o.created_at DESC
LIMIT 1;Vous attendez status = 'pending' et total_amount correspondant à ce que l'UI affichait. Si le total diffère d'un centime, vous avez trouvé un bug d'arrondi. Si le statut est autre que 'pending', vous avez trouvé un bug de transition d'état.
Pour les assertions sur le nombre de lignes, vérifier que exactement N enregistrements ont été créés, mis à jour ou supprimés :
-- Vérifier qu'exactement trois articles de commande ont été créés pour cette commande
SELECT COUNT(*) AS item_count
FROM order_items
WHERE order_id = 1001;
-- Vérifier qu'il n'existe pas d'enregistrement utilisateur dupliqué pour cet email
SELECT COUNT(*) AS duplicate_count
FROM users
WHERE email = 'testuser@test.com';Ces requêtes sont prêtes pour les assertions. Dans un test automatisé, vous comparez le nombre renvoyé à la valeur attendue. En test manuel, vous les lancez dans votre client SQL et examinez le résultat.
Configurer les données de test avec INSERT, et les nettoyer
Passer par l'UI pour créer des données de test est lent. Un test qui nécessite un utilisateur avec cinq commandes terminées, deux commandes en attente, et une commande annulée peut prendre dix minutes à configurer manuellement. Avec SQL, ça prend quelques secondes.
-- Créer un utilisateur de test
INSERT INTO users (email, password_hash, role, is_active, created_at)
VALUES (
'qa_load_test_user@test.com',
'$2b$10$placeholder_hash_value',
'customer',
true,
NOW()
);
-- Obtenir l'ID du nouvel utilisateur pour les insertions suivantes
SELECT id FROM users WHERE email = 'qa_load_test_user@test.com';
-- Créer des commandes pour cet utilisateur (en supposant id = 99)
INSERT INTO orders (customer_id, status, total_amount, created_at)
VALUES
(99, 'completed', 89.99, NOW() - INTERVAL '10 days'),
(99, 'completed', 124.50, NOW() - INTERVAL '7 days'),
(99, 'completed', 45.00, NOW() - INTERVAL '4 days'),
(99, 'pending', 67.25, NOW() - INTERVAL '1 day'),
(99, 'pending', 210.00, NOW()),
(99, 'cancelled', 30.00, NOW() - INTERVAL '5 days');Cela place la base de données dans exactement l'état que votre test nécessite, sans cliquer sur aucune UI. Le test peut maintenant vérifier des comportements qui dépendent de l'historique des commandes.
Le nettoyage est tout aussi important. Les données de test laissées dans des bases partagées causent une pollution des tests. Un test ultérieur trouve des enregistrements inattendus, les comptages reviennent incorrects, et vous passez une heure à déboguer quelque chose qui n'était pas cassé.
-- Nettoyer après les tests avec une convention de nommage cohérente
DELETE FROM orders WHERE customer_id IN (
SELECT id FROM users WHERE email LIKE '%@test.com'
);
DELETE FROM users WHERE email LIKE '%@test.com';La convention compte : utilisez un domaine cohérent comme @test.com ou un préfixe comme qa_ pour toutes les données générées par les tests. Cela rend les requêtes de nettoyage sûres et fiables.
SELECT * FROM users WHERE email LIKE '%@test.com' avant DELETE FROM users WHERE email LIKE '%@test.com' prend cinq secondes et peut éviter une très mauvaise journée.JOINs pour le QA : trouver les enregistrements orphelins et vérifier les relations
Les bases relationnelles imposent des relations via des clés étrangères, mais les bugs surviennent à la frontière entre les tables. Un article de commande peut référencer un produit qui a été supprimé. Un enregistrement de session peut pointer vers un utilisateur qui n'existe plus. Ces enregistrements orphelins causent des échecs silencieux difficiles à tracer sans regarder directement les données.
LEFT JOIN expose ces lacunes. Contrairement à un JOIN classique qui ne renvoie que les lignes avec correspondance des deux côtés, LEFT JOIN renvoie toutes les lignes de la table gauche. Les colonnes de la table droite sont NULL quand aucune correspondance n'existe :
-- Trouver les articles de commande sans produit correspondant (enregistrements orphelins)
SELECT
order_items.id AS item_id,
order_items.order_id,
order_items.product_id,
products.name AS product_name
FROM order_items
LEFT JOIN products ON order_items.product_id = products.id
WHERE products.id IS NULL;Toute ligne renvoyée est un article de commande orphelin : il référence un produit qui n'existe pas. Dans une base saine, cette requête renvoie zéro lignes.
-- Trouver les commandes sans client (ne devrait pas être possible, mais vaut la peine de vérifier)
SELECT orders.id, orders.customer_id, orders.total_amount
FROM orders
LEFT JOIN users ON orders.customer_id = users.id
WHERE users.id IS NULL;
-- Vérifier que chaque commande a au moins un article
SELECT
o.id AS order_id,
o.status,
COUNT(oi.id) AS item_count
FROM orders o
LEFT JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id, o.status
HAVING COUNT(oi.id) = 0;Lancez ces requêtes après des scénarios de test impliquant une suppression, un archivage, ou des opérations en masse. Les règles de suppression en cascade sont faciles à mal configurer, et un enregistrement orphelin qui aurait dû être nettoyé peut causer des erreurs applicatives des jours plus tard.
Requêtes d'agrégation : vérifier les calculs que l'UI affiche
Quand votre application affiche un total, une moyenne, un nombre, ou un résumé, vous pouvez le vérifier directement contre la base. Si l'UI affiche "Chiffre d'affaires : 4 827,50 €" pour le mois dernier, la base doit être d'accord.
-- Vérifier le chiffre d'affaires total pour une période
SELECT
SUM(total_amount) AS total_revenue,
COUNT(*) AS order_count,
AVG(total_amount) AS average_order_value
FROM orders
WHERE status = 'completed'
AND created_at >= '2026-05-01'
AND created_at < '2026-06-01';
-- Vérifier la ventilation du chiffre d'affaires par produit
SELECT
p.name AS product_name,
SUM(oi.quantity) AS units_sold,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.id
JOIN orders o ON oi.order_id = o.id
WHERE o.status = 'completed'
GROUP BY p.id, p.name
ORDER BY revenue DESC;Ces requêtes deviennent puissantes quand le calcul de l'UI et celui de la base divergent. Un écart signifie soit que l'UI calcule quelque chose incorrectement, soit que la requête qui alimente l'UI a un bug. Il est aussi possible que les données de test elles-mêmes soient dans un état inattendu.
-- Vérifier que les stocks correspondent à ce que la page produit affiche
SELECT
id,
name,
stock_quantity,
CASE WHEN stock_quantity = 0 THEN 'out_of_stock'
WHEN stock_quantity < 10 THEN 'low_stock'
ELSE 'in_stock'
END AS computed_status
FROM products
WHERE id IN (101, 102, 103);La colonne computed_status vous permet de comparer ce que SQL calcule avec ce que l'UI affiche.
Se connecter à une base depuis un test
Pour une vérification manuelle pendant le débogage, un client SQL comme TablePlus ou DBeaver est la voie la plus rapide. Connectez-vous à la base, lancez votre requête, lisez le résultat. Mais pour les suites de tests automatisées, vous voulez que la vérification de base de données se fasse dans le test lui-même.
Depuis la ligne de commande avec psql :# Lancer une seule requête et obtenir le résultat
psql $DATABASE_URL -c "SELECT COUNT(*) FROM orders WHERE status = 'pending';"
# Lancer un fichier de requête
psql $DATABASE_URL -f verify_order_state.sqlimport { test, expect } from '@playwright/test';
import { Client } from 'pg'; // npm install pg
test('la commande est sauvegardée en base après le paiement', async ({ page }) => {
const testEmail = 'qa_checkout_test@test.com';
// Effectuer l'action UI
await page.goto('/checkout');
// ... remplir le panier, saisir la livraison, soumettre la commande ...
// Vérifier le résultat en base de données
const db = new Client({ connectionString: process.env.DATABASE_URL });
await db.connect();
try {
const result = await db.query(
`SELECT o.id, o.status, o.total_amount
FROM orders o
JOIN users u ON o.customer_id = u.id
WHERE u.email = $1
ORDER BY o.created_at DESC
LIMIT 1`,
[testEmail]
);
expect(result.rows).toHaveLength(1);
expect(result.rows[0].status).toBe('pending');
expect(parseFloat(result.rows[0].total_amount)).toBeCloseTo(89.99, 2);
} finally {
await db.end();
}
});Le $1 est important. Ne concaténez jamais des valeurs contrôlées par l'utilisateur dans une chaîne SQL ; utilisez des requêtes paramétrées pour toute valeur provenant des entrées de test ou de données externes.
.env.test et ne la committez jamais. Utilisez process.env.DATABASE_URL dans le code de test et ajoutez .env.test brut à .gitignore. Votre pipeline CI doit injecter la valeur via des variables d'environnement, pas via des fichiers committés.Un pattern pratique pour les suites plus grandes est de créer des helpers de base de données à côté de vos page objects :
// helpers/db.ts
import { Client } from 'pg';
export async function getLatestOrderForUser(email: string) {
const db = new Client({ connectionString: process.env.DATABASE_URL });
await db.connect();
try {
const result = await db.query(
`SELECT o.* FROM orders o
JOIN users u ON o.customer_id = u.id
WHERE u.email = $1
ORDER BY o.created_at DESC LIMIT 1`,
[email]
);
return result.rows[0] ?? null;
} finally {
await db.end();
}
}
export async function cleanupTestUser(email: string) {
const db = new Client({ connectionString: process.env.DATABASE_URL });
await db.connect();
try {
await db.query(
`DELETE FROM orders WHERE customer_id IN
(SELECT id FROM users WHERE email = $1)`,
[email]
);
await db.query('DELETE FROM users WHERE email = $1', [email]);
} finally {
await db.end();
}
}Puis dans votre test :
import { getLatestOrderForUser, cleanupTestUser } from '../helpers/db';
test.afterEach(async () => {
await cleanupTestUser('qa_checkout_test@test.com');
});
test('le statut de commande est en attente après le paiement', async ({ page }) => {
// ... actions UI ...
const order = await getLatestOrderForUser('qa_checkout_test@test.com');
expect(order).not.toBeNull();
expect(order.status).toBe('pending');
});Cela garde le SQL hors du corps du test et rend les assertions lisibles.
Règles de sécurité pour l'accès à la base en test
Quelques règles qui évitent les incidents qui gâchent une journée de travail.
Utilisez toujours WHERE sur UPDATE et DELETE. Ça mérite d'être répété. L'erreur SQL la plus dangereuse est d'exécuter une requête modificatrice sans filtre. Avant d'exécuter tout UPDATE ou DELETE, relisez-le et confirmez que la clause WHERE est présente et correcte. Lancez SELECT avant DELETE pour le nettoyage. Si vous êtes sur le point de supprimer des données de test, lancez la version SELECT de la requête d'abord pour voir exactement ce qui sera supprimé. Si le résultat vous semble correct, changez SELECT en DELETE et lancez-la. Sachez à quel environnement vous êtes connecté. La plupart des clients SQL affichent la connexion courante dans l'en-tête ou l'onglet. Avant toute requête d'écriture, confirmez que vous êtes sur une base de développement ou de staging, pas la production. Une convention de nommage claire pour les bases (par ex.myapp_dev, myapp_staging, myapp_prod) facilite la vérification.
Utilisez des transactions pour la configuration multi-étapes des données de test. Quand votre configuration insère dans plusieurs tables, enveloppez-la dans une transaction pour que les échecs partiels ne laissent pas la base dans un état cassé :
BEGIN;
INSERT INTO users (email, role, is_active)
VALUES ('qa_transaction_test@test.com', 'customer', true);
INSERT INTO orders (customer_id, status, total_amount)
VALUES (LASTVAL(), 'pending', 99.00);
-- Si quoi que ce soit ci-dessus a échoué, ceci annule les deux insertions
COMMIT;FAQ
Dois-je comprendre la conception de base de données pour écrire du SQL QA ?Non. Vous devez comprendre les tables que votre application utilise : leur nom, leurs colonnes, comment elles se rapportent les unes aux autres. Ça vient de la lecture du schéma (la plupart des clients SQL l'affichent dans une barre latérale), en interrogeant un développeur, ou en consultant les modèles ORM dans la codebase.
Quelle est la différence entre INNER JOIN et LEFT JOIN ?INNER JOIN (ou juste JOIN) renvoie les lignes uniquement où les deux tables ont un enregistrement correspondant. LEFT JOIN renvoie toutes les lignes de la table gauche, avec NULL dans les colonnes de la table droite où aucune correspondance n'existe. En pratique QA : utilisez JOIN pour voir les données combinées, utilisez LEFT JOIN pour trouver les relations manquantes.
Dois-je tester contre une base de données de test dédiée ou la base de développement partagée ?Une base dédiée est fortement préférable. Quand plusieurs développeurs et pipelines de test partagent une même base, les données de test de différents runs se mélangent. Les assertions sur les comptages ou l'état deviennent alors peu fiables.
Ma requête ne renvoie aucune ligne mais je sais que les données sont là. Qu'est-ce qui ne va pas ?Quatre causes courantes. La valeur filtrée peut avoir une casse différente de celle en base (WHERE email = 'Test@example.com' ne correspondra pas à test@example.com en collation sensible à la casse). Des espaces en début ou fin de valeur de colonne sont une autre cause fréquente. Vous êtes peut-être aussi connecté à la mauvaise base ou au mauvais schéma. Enfin, les données ont peut-être été insérées dans une transaction non committée qu'une autre connexion ne peut pas encore voir.
Dans PostgreSQL : SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';. Dans MySQL, SHOW TABLES;. La plupart des clients GUI SQL affichent également le schéma dans une vue arborescente sur la gauche.