Entretien QA : construire une stratégie de test autour d’une API métier
Quick Overview
Un cas original de remboursement pour construire une stratégie QA : contrat HTTP, invariant financier, autorisation entre commerçants, états incertains et rejeu. Vingt-deux assertions locales HTTP/SQLite vérifient les effets et la concurrence, avec des limites explicites sur le prestataire absent.
Une API de remboursement renvoie 202. Le test qui vérifie seulement le statut est vert. Pourtant, deux demandes simultanées peuvent réserver plus d’argent que le paiement initial, ou un délai dépassé peut déclencher un second remboursement. En entretien QA, partez de ce que le système doit préserver, puis choisissez les requêtes et les totaux qui révèlent une violation.
Ce guide propose une API fictive, un contrat explicite et des tests exécutés localement. Il ne décrit pas le processus d’un employeur et ne reproduit aucune épreuve confidentielle. Les faits issus de normes sont signalés et liés ; les montants, états et décisions de priorité appartiennent à notre exercice original. Aucun témoignage de candidat n’est utilisé comme preuve d’un format d’entretien.
Sur PracHub, Prevent Duplicate Payments Under High Load permet de prolonger ce raisonnement : distinguer une réponse reçue d’un effet financier effectivement réalisé.

Clarifier le contrat avant de choisir les outils
Le paiement p1 appartient au commerçant A. Il contient 10 000 centimes capturés en EUR, dont 2 000 déjà remboursés. Il reste donc 8 000 centimes disponibles avant toute nouvelle demande. Le commerçant B ne doit ni consulter ce paiement ni créer un remboursement dessus. Ces identités sont fictives ; les jetons du programme servent seulement à sélectionner un acteur de test.
La route POST /payments/p1/refunds reçoit un objet JSON contenant uniquement amount_minor et currency, ainsi qu’un en-tête Idempotency-Key. Le montant doit être un entier strictement positif, exprimé en centimes, et la devise doit correspondre au paiement. Une clé identifie une intention de remboursement dans le périmètre du commerçant. Notre contrat limite sa longueur à 64 caractères et n’implémente aucune expiration.
Avant de tester une API réelle, demandez si les remboursements partiels sont autorisés, quels paiements sont éligibles, quel acteur possède quels droits et combien de temps les clés restent conservées. Vérifiez aussi le comportement attendu quand le prestataire a peut-être traité la demande sans répondre. Dessinez les transitions pour rendre visibles les effets encore incertains.
Voici les réponses choisies pour cet exercice. Elles ne constituent pas une convention obligatoire pour toutes les API de paiement.
| Situation | Réponse choisie | Observation complémentaire |
|---|---|---|
| Nouvelle intention valide | 202, identifiant, état PENDING | Une réservation existe dans le registre |
| Même clé et même empreinte | 200, même identifiant | Aucune nouvelle réservation |
| Même clé, contenu différent | 409 | L’intention initiale reste inchangée |
| Identité absente ou inconnue | 401 | Aucun remboursement créé |
| Paiement ou remboursement d’un autre commerçant | 404 | Aucun objet exposé ni modifié |
| JSON illisible | 400 | Aucun effet métier |
| Montant, devise ou clé invalide | 422 | Aucun effet métier |
| Disponible insuffisant | 409 | Aucune réservation supplémentaire |
Fait normatif : selon la RFC 9110, section 15.3.3, 202 Accepted indique une acceptation pour traitement, pas une exécution achevée. Notre choix d’exposer un identifiant consultable est une décision de conception de l’exercice. Le test doit donc lire l’état et vérifier les effets, pas interpréter ce statut comme « argent remboursé ».
Construire l’oracle autour du montant réservé
Un oracle est le critère qui distingue le résultat attendu d’un défaut. Ici, l’invariant principal est : total confirmé remboursé, plus montant réservé, inférieur ou égal au montant capturé. Les réservations comprennent les états PENDING et UNKNOWN. Les états terminaux sont SUCCEEDED et FAILED.
Une première demande de 6 000 centimes fait passer le disponible de 8 000 à 2 000. Si son résultat devient inconnu, les 6 000 restent réservés. Une autre demande de 2 001 doit être refusée. Si la première demande réussit, le total remboursé devient 8 000 et sa réservation disparaît du calcul. Si elle échoue de manière certaine, la réservation est libérée et le total confirmé reste 2 000.
Dans ce modèle, répéter une confirmation de succès ne doit pas ajouter une seconde fois le montant. Une confirmation d’échec après un succès terminal est contradictoire : elle produit un conflit à investiguer, sans annuler silencieusement le remboursement. Le modèle refuse également de remplacer un échec terminal par un succès. Un véritable prestataire pourrait avoir des règles différentes ; il faudrait alors définir leur traduction avant d’écrire les assertions.

Une réponse orale possible : « Je vérifie d’abord que les états incertains continuent à consommer le disponible. Ensuite, je rejoue le résultat terminal et je contrôle le registre. Le statut HTTP seul ne prouve ni l’absence de double effet ni la bonne réconciliation. » Cette réponse transforme un risque abstrait en trois observations vérifiables.
Donner la priorité aux dommages plutôt qu’au nombre de cas
Commencez par les scénarios dont un échec peut exposer un objet ou créer un effet financier excessif. Dans notre cas, l’accès entre commerçants, la concurrence sur le disponible et le double règlement passent avant la ponctuation d’un message d’erreur. C’est une proposition de priorité pour ce contrat, pas un classement universel des risques.
Fait de sécurité : OWASP API1:2023 décrit les défauts d’autorisation au niveau des objets. Une identité authentifiée ne suffit pas à autoriser l’accès à chaque identifiant fourni. Pour notre exercice, cela conduit à tester A et B sur les routes de création et de consultation, en vérifiant aussi l’absence de mutation après un refus.
Puis viennent les limites qui peuvent invalider l’oracle : zéro, valeur négative, chaîne numérique, booléen, devise différente et dépassement d’un centime. Un booléen mérite une assertion distincte : en Python, bool est un sous-type de int. La fixture utilise donc type(value) is int, afin de respecter le contrat JSON choisi, plutôt qu’un contrôle qui accepterait true comme un centime.
Présentez votre plan avec un risque, une préparation, une action et une preuve pour chaque scénario. Pour le refus entre commerçants, préparez A et B, tentez la création puis la lecture, et contrôlez le registre inchangé. Pour une issue incertaine, injectez cet état, demandez un montant supérieur au disponible résiduel et vérifiez le refus. Pour le rejeu, comparez identifiant et nombre de lignes, pas seulement deux réponses identiques. L’évaluateur peut contester une hypothèse précise à partir de votre préparation et de vos preuves. Elle aide aussi à distinguer un test manquant d’un test présent mais doté d’un oracle trop faible.
Enfin, prévoyez les risques non couverts par ce petit programme : identité réelle et expiration des jetons, droits fonctionnels, indisponibilité du stockage, limites de taille, cadence des demandes, observabilité et compatibilité des consommateurs. Les reconnaître ne signifie pas inventer des résultats. Présentez pour chacun le dispositif nécessaire et le critère de réussite encore manquant.
Exécuter un test de contrat qui regarde aussi l’effet métier
La fixture ouvre un serveur HTTP sur 127.0.0.1, avec un port attribué par le système et une base SQLite temporaire sur disque. Les requêtes traversent réellement HTTP, le décodage JSON, la sélection du commerçant et la transaction locale. Le prestataire financier est absent : les résultats sont injectés directement par une fonction interne de réconciliation, sans endpoint public correspondant.
Le client standard Python lit le statut et le JSON, y compris pour les réponses d’erreur. Voici une partie effectivement exécutée du scénario, après les vérifications d’authentification et de validation :
status, r = request(base)
rid = r['id']
assert status == 202 and r['state'] == 'PENDING'
assert request(base) == (200, r)
assert request(base, amount=6001)[0] == 409
settle(db, rid, 'UNKNOWN')
assert request(base, amount=2001, key='r2')[0] == 409
Chaque assertion a une raison différente. La première établit l’acceptation et l’état initial. La deuxième vérifie l’identité stable et la réponse au rejeu. La troisième distingue une répétition d’une modification de l’intention. La dernière montre qu’une issue incertaine garde une réservation. Ajoutez une lecture du registre pour confirmer le nombre d’objets et le total financier ; une réponse stable pourrait autrement masquer un double effet interne.
La suite complète vérifie également un succès répété, le total confirmé de 8 000, un résultat terminal contradictoire et l’acceptation des 2 000 restants. Un autre scénario neuf vérifie qu’un échec certain libère la réservation. Les bases sont séparées : un test ne dépend pas des remboursements laissés par le précédent.
La suite contient 22 assertions nommées. Le fichier checks.json conserve les versions du runtime, les noms des assertions et la portée de l’exécution. Ces preuves portent sur la fixture HTTP et SQLite locale. Elles ne prouvent ni un transfert d’argent ni la fiabilité d’un fournisseur externe, puisque ces composants ne participent pas au test.
Tester deux demandes concurrentes sans promettre l’exactement-une-fois
Le scénario concurrent utilise deux clients et une barrière de départ. Chacun demande 6 000 centimes sur le même paiement, avec une clé différente. Les 8 000 disponibles permettent une seule acceptation. Le résultat attendu est un 202 et un 409, puis une seule ligne de remboursement réservant 6 000.
Deux appels lancés successivement pourraient passer sans exercer le conflit recherché. La barrière évite d’attendre volontairement la fin du premier avant de démarrer le second ; elle ne garantit pas une simultanéité à la nanoseconde. Les deux routes passent ensuite par des connexions SQLite distinctes et sérialisent leurs écritures.
Fait d’implémentation documenté : SQLite décrit BEGIN IMMEDIATE comme le démarrage immédiat d’une transaction d’écriture ; une autre écriture active peut provoquer SQLITE_BUSY. Notre fixture utilise un délai d’attente de cinq secondes et inclut lecture du disponible, décision et insertion dans la même transaction. Le test HTTP local a obtenu un 202, un 409 et une seule réservation de 6 000 centimes. Il ne mesure pas le débit, la contention prolongée ou le comportement d’un autre moteur.
Pour une application distribuée, demanderiez-vous un test de crash après l’effet distant mais avant son enregistrement local ? Oui : il expose une frontière que SQLite seule ne résout pas. Il faudrait définir les garanties du fournisseur, la persistance de l’intention et la réconciliation. Ne concluez pas « exactement une fois » à partir de deux requêtes réussies dans une fixture locale.
Présenter un défaut et une décision de livraison
Supposons qu’une version libère la réservation dès qu’un appel expire. Votre rapport doit donner le paiement initial, la demande de 6 000, le passage à UNKNOWN, puis une seconde demande de 2 001 acceptée à tort. Le résultat attendu est son refus, puisque le premier effet peut encore réussir. Incluez les identifiants fictifs, la chronologie et les totaux ; évitez les jetons réels et les données personnelles.
Pour décider de livrer, distinguez le défaut reproduit des zones non évaluées. Vous pouvez proposer de bloquer cette version sur la violation du disponible, puis de vérifier le correctif avec le même contre-exemple. Vous ne pouvez pas déclarer tout le système sûr parce que la régression passe. Une revue des droits réels et un scénario de reprise après panne restent nécessaires avant une conclusion plus large.
Expliquez aussi le coût de votre choix : garder une réservation incertaine évite de surestimer le disponible, mais peut immobiliser une somme jusqu’à réconciliation. Le produit doit prévoir un suivi et une responsabilité opérationnelle. L’automatisation vérifie la règle retenue ; elle ne décide pas seule du délai acceptable pour un commerçant.
Prolonger l’exercice sur PracHub
Ces questions publiques complètent des parties différentes du raisonnement. Elles ne sont pas présentées comme une banque officielle d’épreuves QA en français.
| Question PracHub | Travail à produire |
|---|---|
| Prevent Duplicate Payments Under High Load | Distinguer clé, empreinte, réservation et effet confirmé |
| Design a Payment Processing System with Exactly-Once Charging | Localiser les frontières où une panne laisse une issue inconnue |
| Validate Unit-Test Coverage and Identify Missing Scenarios | Relier chaque assertion à un risque, puis nommer les omissions |
| Build an API aggregator with concurrency and retries | Expliquer pourquoi délai dépassé et absence d’effet diffèrent |
| Design authorization and audit logging systems | Séparer identité, propriété de l’objet et preuve d’accès |
Pour vous entraîner, commencez par Prevent Duplicate Payments Under High Load. Énoncez l’invariant, dessinez les états et défendez votre premier test. Votre objectif est de rendre une conclusion contrôlable avec des entrées, des effets et des limites explicites.
Sources and Further Reading
- RFC 9110 — 202 Accepted : sens du statut asynchrone, sans garantie d’achèvement.
- OWASP API1:2023 — Broken Object Level Authorization : autorisation liée à chaque objet manipulé.
- SQLite — Transactions : modes de transaction et limites de concurrence du moteur local.
- Python — Boolean type : relation entre booléens et entiers utilisée pour choisir le validateur de la fixture.
Comments (0)