Ce que personne ne vous dit. Les vrais pièges, les vraies solutions, par un architecte QA terrain.
Il y a une question que personne ne pose jamais en début de projet. Une question tellement évidente qu'elle passe sous le radar de tous les comités d'architecture, de tous les appels d'offre, de tous les "kick-off meetings" saupoudrés de slides PowerPoint : où sont vos tests ?
Pas "avez-vous des tests" — ça, tout le monde dit oui. Non. Où sont-ils ? Dans quel outil ? Dans quel format ? Et surtout : qui peut les retrouver à 3h du matin quand la prod est en feu ?
En 20 ans de missions — de la Défense nationale aux biotechnologies, de l'aérospatiale à la grande distribution — j'ai vu la même scène se répéter des dizaines de fois. L'équipe dev utilise Jira. L'équipe QA utilise TestRail. Ou Zephyr. Ou HP ALM. Ou, dans les cas les plus désespérants, un fichier Excel partagé sur un drive quelque part, avec 47 onglets et des cellules colorées en rouge-orange-vert selon un code que seul le créateur original (parti depuis 2 ans) comprenait.
Le résultat est toujours le même : deux mondes parallèles. Les développeurs vivent dans Jira. Les testeurs vivent ailleurs. Et entre les deux ? Un gouffre. Un gouffre qu'on essaie de combler avec des "synchros hebdo", des exports CSV, des réunions de status où quelqu'un dit "oui oui, c'est testé" sans que personne ne puisse le vérifier.
Une grande banque européenne gérait 58 000 scénarios de test dans HP Quality Center. 200 000 steps. Des équipes de 100+ testeurs réparties sur 3 pays. Le jour où ils ont voulu comprendre quels tests couvraient une exigence réglementaire spécifique — une question de conformité bancaire, pas un caprice — il a fallu 3 semaines pour reconstituer la traçabilité.
Trois semaines. Pour une question qui, dans un système bien conçu, prend 30 secondes avec un filtre JQL.
Leur migration vers Xray/Jira a pris 3-4 mois. Mais après : traçabilité complète, audit en temps réel, et surtout — les devs et les testeurs travaillaient enfin dans le même outil. Le premier "quick win" inattendu ? Les développeurs ont commencé spontanément à créer des tests, parce que c'était devenu aussi simple que créer un ticket Jira.
Le marché du test management est en pleine transformation. Atlassian a racheté, restructuré, imposé la migration Cloud. Les outils historiques (HP ALM, TestLink) sont en déclin. Et dans l'écosystème Jira, trois acteurs dominent :
| Critère | Xray | Zephyr Scale | TestRail |
|---|---|---|---|
| Intégration Jira | Native (issue types Jira) | Plugin (panels custom) | Externe (connecteur) |
| Issue Types propres | ✅ Test, Precondition, Test Set, Test Plan, Test Execution | ❌ Panels séparés dans les issues | ❌ Outil externe |
| JQL sur les tests | ✅ Recherche native + fonctions custom | ⚠️ Limité | ❌ Pas applicable |
| Workflows Jira | ✅ Workflows standard Jira sur les tests | ⚠️ Workflows séparés | ❌ Workflows propres à TestRail |
| REST API | v1 + v2 (DC), REST + GraphQL (Cloud) | REST API | REST API |
| CI/CD natif | Jenkins, GitLab, Bamboo, GitHub Actions | Jenkins, Bamboo | Via API uniquement |
| Pricing (Cloud, 50 users) | ~$250-300/mois (Standard) | ~$250/mois | ~$600/mois (séparé de Jira) |
| Marketplace rank | #1 test management (fastest growing) | #2 (ex-TM4J, racheté SmartBear) | Hors Marketplace |
Xray n'est pas parfait. Le reporting natif est son talon d'Achille — vous aurez besoin d'eazyBI ou Custom Charts pour des dashboards dignes de ce nom. L'interface peut être déroutante au début (trop de concepts). Et si vous n'êtes pas déjà sur Jira, n'adoptez pas Jira juste pour Xray — c'est mettre la charrue avant les bœufs.
Mais si votre équipe vit déjà dans Jira au quotidien ? L'argument "natif" d'Xray est imbattable. Un test Xray est un ticket Jira. Pas un panel greffé, pas un lien vers un outil externe. Un ticket. Avec son workflow, ses custom fields, son JQL, ses notifications, ses boards. C'est cette simplicité conceptuelle qui fait la vraie différence sur le terrain.
Avant de foncer sur un outil, il faut savoir où vous en êtes. Voici les 5 niveaux de maturité que j'observe sur le terrain :
C'est la section que vous ne trouverez dans aucune documentation officielle. Mais elle est cruciale pour éviter des erreurs coûteuses :
Atlassian a introduit un modèle "App Editions" que Xray a adopté. Voici la cartographie réelle :
| Édition | Cible | Features clés | Pricing indicatif |
|---|---|---|---|
| Standard (Cloud) | PME, équipes en croissance | Toutes les features core : issue types, traçabilité, CI/CD, Test Case Importer, reporting de base | ~$10/mois (10 users), ~$5-6/user ensuite |
| Advanced (Cloud) | Mid-market, teams matures | Standard + insights d'adoption, AI étendue, storage/API augmentés, test design avancé | Premium sur Standard (TBD) |
| Enterprise (app séparée) | Grandes organisations | Versioning/approvals des tests, Dynamic Test Plans, Remote Job Triggers, AI Test Model Generation (Sembi IQ) | ~$15/mois (10 users), ~$6.50/user ensuite |
| Data Center | On-premise, régulé | Équivalent Enterprise, hébergement propre, REST API v1+v2, PAT auth | Licence annuelle (négociable) |
Parlons chiffres concrets, pas de slides commercial. Quand une équipe passe d'Excel/TestLink à Xray bien configuré, voici ce qu'on observe réellement sur le terrain :
| Métrique | Avant (Excel/outil séparé) | Après (Xray intégré) | Impact |
|---|---|---|---|
| Temps pour trouver un test lié à une exigence | 15-45 minutes (recherche manuelle) | 10 secondes (JQL / lien direct) | 🔥 -98% |
| Temps de création de rapport de couverture | 2-4 heures (copier-coller) | 30 secondes (gadget Jira) | 🔥 -99% |
| Résultats auto dans le test management | Import manuel J+1 | Temps réel (CI/CD → Xray) | ✅ Temps réel |
| Visibilité dev sur les tests | Quasi nulle | Même board, mêmes filtres | ✅ Collaboration |
| Préparation audit conformité | 2-3 semaines | Quelques heures (exports automatisés) | 🔥 -90% |
Les Chemins de fer fédéraux suisses (SBB) sont le cœur du transport public helvétique. Plus de 600 applications, 100 000+ cas de test, 1 000+ employés formés. En 2018, lors de leur transformation agile, ils ont choisi Xray pour centraliser tout leur test management. Le point clé ? Ils testent à la fois du logiciel et du matériel — signalisation, billetterie, affichage en gare. Xray leur a permis de tout unifier dans un seul référentiel, avec une traçabilité complète du requirement hardware au test d'intégration.
Voilà pour le contexte. Vous avez maintenant la carte du territoire. Vous savez pourquoi centraliser, quand le faire (et quand ne pas le faire), combien ça coûte, et ce que ça rapporte concrètement.
Dans l'onglet suivant, on va plonger dans le sujet que personne n'ose aborder : les 10 anti-patterns qui font échouer 80% des implémentations Xray. Ce sont des erreurs que j'ai vues et corrigées sur le terrain — pas de la théorie de consultant.
Installer Xray prend 5 minutes. Le configurer correctement prend 5 jours. Le massacrer prend environ 2 sprints.
Ce qui suit n'est pas un catalogue théorique. Ce sont 10 patterns destructeurs que j'ai observés — et souvent corrigés — sur des projets réels. Chacun est accompagné de ses symptômes, de son histoire, et surtout de sa solution. Parce que repérer un anti-pattern sans donner le fix, c'est comme diagnostiquer une maladie sans prescrire le traitement.
C'est l'anti-pattern le plus répandu et le plus sournois. Des milliers de tests existent dans Xray, mais aucun n'est relié à une exigence, une user story, ou un epic. Ils flottent dans le vide, comme des satellites sans orbite.
Une équipe retail avait migré 2 500 tests depuis TestLink. Import CSV parfait, tous les steps préservés. Mais personne n'avait recréé les liens requirement↔test. Résultat : 6 mois après la migration, le management a demandé un rapport de couverture pour un audit. Réponse de l'équipe : "on ne peut pas le générer". Le management a conclu que Xray ne servait à rien et a failli annuler les licences. Le problème n'était pas l'outil — c'était l'absence de traçabilité.
issueFunction in requirements() AND NOT issueFunction in tested() pour trouver les exigences non couvertes.
Celui-là est vicieux. L'équipe exécute des tests — manuels ou automatisés — mais ne crée jamais de Test Execution dans Xray. Les testeurs exécutent "de tête", notent les résultats dans Confluence, dans Slack, ou nulle part. Les tests automatisés tournent dans Jenkins mais les résultats ne remontent pas.
POST /api/v2/import/execution/junit). Un résultat de test qui n'est pas dans Xray n'existe pas.
Jenkinsfile
// Exemple Jenkins : push résultats JUnit vers Xray Cloud
stage('Report to Xray') {
steps {
step([$class: 'XrayImportBuilder',
endpointName: '/junit',
importFilePath: 'target/surefire-reports/*.xml',
serverInstance: 'xray-cloud-instance',
projectKey: 'PROJ',
testPlanKey: 'PROJ-1234'
])
}
}
Personne n'ose supprimer un test. "Et si on en a besoin plus tard ?" Alors les tests s'accumulent. Des tests pour des features supprimées il y a 3 ans. Des tests dupliqués (version manuelle + version auto). Des tests qui échouent systématiquement et que tout le monde ignore.
issuetype = Test AND created < -180d AND NOT issueFunction in executedTests(). Créez un label DEPRECATED et un workflow status "Archived". Les tests obsolètes ne sont pas supprimés (traçabilité) mais exclus des Test Plans actifs. Objectif : < 20% de tests non exécutés dans les 6 derniers mois.
L'admin Jira enthousiaste découvre qu'on peut ajouter des custom fields aux issues Xray. Alors il en crée. Pour le navigateur. Pour l'environnement. Pour la priorité de test (en plus de la priorité Jira). Pour la complexité. Pour le "test type" (en plus du type Xray). Pour la date prévue d'exécution. Pour le nom du testeur assigné (en plus de l'assignee Jira)...
Au lieu d'utiliser les Test Environments et les Test Plans versionnés, l'équipe crée des copies de chaque test : "Login - Chrome", "Login - Firefox", "Login - Safari", "Login - v2.1", "Login - v2.2"... Le Test Repository explose en taille, et quand il faut modifier un step, il faut le faire dans 12 copies.
Au lieu de créer un Test Plan par sprint, par release, ou par campagne, l'équipe maintient UN Test Plan géant qui grossit indéfiniment. 500 tests, 800 tests, 1 200 tests... Le Test Plan devient un monstre ingérable où les résultats du sprint 1 se mélangent avec ceux du sprint 47.
L'équipe a commencé par des tests manuels. Puis l'automatisation est arrivée. Au lieu de convertir les tests manuels en tests automatisés (en changeant le Test Type), on a créé des tests auto en parallèle. Résultat : chaque scénario a deux entrées dans Xray. La couverture est comptée en double. Les résultats se contredisent.
L'outil est en place, les tests sont exécutés, les résultats sont enregistrés... mais personne ne les regarde. Pas de dashboard. Pas de gadgets. Pas de rapports automatisés. Les données sont là, mais invisibles. Le management demande des "KPIs qualité" et l'équipe QA ouvre Excel pour les calculer manuellement.
Le reporting natif d'Xray est son point faible reconnu. Les gadgets de base sont limités. Pour des dashboards réellement exploitables, vous aurez besoin d'eazyBI (~$10-50/mois selon la taille) ou de Custom Charts for Jira. C'est un coût additionnel que personne ne mentionne au moment de la vente. Prévoyez-le dans votre budget dès le départ.
Xray permet de définir des workflows pour les Tests (Draft → In Review → Approved → Deprecated). Mais personne ne les configure, ou personne ne les utilise. Tous les tests sont "Draft" à vie. Il n'y a aucun process de review, aucune approbation, aucun contrôle qualité sur les tests eux-mêmes.
issuetype = Test AND status = Draft AND created < -14d pour repérer les tests oubliés en Draft. Pour les organisations réglementées, l'édition Enterprise offre le versioning et les approbations formelles.
C'est le plus stratégique de tous les anti-patterns. L'équipe a investi 2 ans à construire un patrimoine de tests dans Xray. 10 000 tests, des centaines de Test Plans, des dashboards, des intégrations CI/CD... Et un jour, Atlassian change les pricing, ou l'entreprise décide de migrer vers Azure DevOps, ou Xray discontinue une feature critique. Et là, la question tombe : "Comment on sort ?"
Python
# Script de backup mensuel - export des tests via API Xray Cloud
import requests
import json
from datetime import datetime
XRAY_CLIENT_ID = "your_client_id"
XRAY_CLIENT_SECRET = "your_client_secret"
# 1. Authentification
auth = requests.post(
"https://xray.cloud.getxray.app/api/v2/authenticate",
json={"client_id": XRAY_CLIENT_ID, "client_secret": XRAY_CLIENT_SECRET}
)
token = auth.json()
# 2. Export des tests via GraphQL
headers = {"Authorization": f"Bearer {token}"}
query = """
{
getTests(jql: "project = PROJ AND issuetype = Test", limit: 100) {
total
results {
issueId
testType { name }
steps { action data result }
preconditions { issueId }
}
}
}
"""
response = requests.post(
"https://xray.cloud.getxray.app/api/v2/graphql",
json={"query": query},
headers=headers
)
# 3. Sauvegarde locale
backup = {
"date": datetime.now().isoformat(),
"data": response.json()
}
with open(f"xray_backup_{datetime.now().strftime('%Y%m%d')}.json", "w") as f:
json.dump(backup, f, indent=2)
print(f"✅ Backup: {response.json()['data']['getTests']['total']} tests exportés")
Si vous reconnaissez 3+ de ces patterns dans votre projet, une revue d'architecture Xray est urgente.
Avant de configurer quoi que ce soit, il faut comprendre comment Xray fonctionne sous le capot. Parce que la majorité des erreurs de configuration viennent d'une incompréhension du modèle de données. Et ce modèle est à la fois la force et la complexité d'Xray.
Quand vous installez Xray dans un projet Jira, 5 nouveaux issue types apparaissent (plus un concept virtuel) :
| Issue Type | Rôle | Analogue réel | Jira natif ? |
|---|---|---|---|
| Test | Le cas de test lui-même — steps, expected results, test data. C'est le template, pas l'exécution. | La recette de cuisine | ✅ Issue type Jira |
| Pre-Condition | Prérequis partageable entre tests : config, données, état initial. Lié à N tests. | Les ingrédients à préparer avant | ✅ Issue type Jira |
| Test Set | Regroupement logique de tests (par fonctionnalité, par module). Organisationnel uniquement. | Un classeur de recettes | ✅ Issue type Jira |
| Test Plan | Campagne de test planifiée. Regroupe des tests pour une version/sprint/release. Calcule les métriques. | Le menu du restaurant pour la semaine | ✅ Issue type Jira |
| Test Execution | L'exécution effective d'un ensemble de tests dans un contexte donné (version, environnement). | La session en cuisine ce soir | ✅ Issue type Jira |
| Test Run | Le résultat d'UN test dans UNE exécution. PASS/FAIL/TODO + evidence + defects. | Le plat terminé (réussi ou raté) | ❌ Entité Xray interne |
Le Test Repository est la vue arborescente qui organise vos tests en dossiers. C'est l'équivalent d'un explorateur de fichiers pour vos cas de test. Et c'est là que beaucoup d'équipes se perdent — parce qu'il n'y a pas UNE bonne organisation, il y a VOTRE organisation.
Voici les 3 patterns d'organisation les plus courants :
📂 Test Repository
├── 📁 Authentication
│ ├── 📁 Login
│ │ ├── 🧪 TC-001: Login with valid credentials
│ │ ├── 🧪 TC-002: Login with invalid password
│ │ └── 🧪 TC-003: Login with 2FA
│ ├── 📁 Registration
│ │ ├── 🧪 TC-010: Register new user
│ │ └── 🧪 TC-011: Register with existing email
│ └── 📁 Password Reset
│ └── 🧪 TC-020: Reset via email link
├── 📁 Cart & Checkout
│ ├── 📁 Cart Management
│ └── 📁 Payment Processing
├── 📁 Admin Panel
│ ├── 📁 User Management
│ └── 📁 Product Management
└── 📁 API Tests
├── 📁 REST Endpoints
└── 📁 GraphQL Queries
Avantage : Aligné avec la structure fonctionnelle de l'application. Facile à naviguer pour les POs et les devs. Mappe naturellement sur les requirements.
📂 Test Repository
├── 📁 Functional Tests
│ ├── 📁 Smoke Tests
│ ├── 📁 Regression Tests
│ └── 📁 Integration Tests
├── 📁 Non-Functional Tests
│ ├── 📁 Performance Tests
│ ├── 📁 Security Tests
│ └── 📁 Accessibility Tests
├── 📁 E2E Tests
│ ├── 📁 Critical Paths
│ └── 📁 Edge Cases
└── 📁 Exploratory
└── 📁 Session-based Charters
Avantage : Clair pour les équipes avec des testeurs spécialisés (perf, sécu, auto). Facilite le reporting par type de test.
Inconvénient : Un même scénario peut apparaître dans plusieurs catégories (un test de login est à la fois "fonctionnel" et "smoke"). Risque de duplication.
📂 Test Repository
├── 📁 Module A - Backend
│ ├── 📁 Unit Tests (auto - Generic)
│ ├── 📁 API Tests (auto - Generic)
│ └── 📁 Integration Tests (auto/manual)
├── 📁 Module B - Frontend
│ ├── 📁 Component Tests (auto - Cucumber)
│ ├── 📁 E2E Tests (auto - Generic)
│ └── 📁 Visual Regression (auto - Generic)
├── 📁 Cross-Module
│ ├── 📁 Smoke Suite
│ ├── 📁 Regression Suite
│ └── 📁 UAT Scenarios (manual)
└── 📁 Non-Functional
├── 📁 Performance
└── 📁 Security
Avantage : Reflète l'architecture technique réelle. Chaque module owner gère ses tests. Scalable.
Xray ajoute des custom fields spéciaux aux issues Jira. Ce sont des champs calculés — ils se mettent à jour automatiquement en fonction des exécutions et de la couverture :
| Custom Field | Visible sur | Ce qu'il calcule | Usage JQL |
|---|---|---|---|
| Requirement Status | Requirements (Story, Epic...) | Couverture de l'exigence basée sur les tests liés et leurs résultats pour une version donnée | "Requirement Status" = OK |
| Test Run Status | Tests | Status agrégé du test basé sur les dernières exécutions pour une version | "TestRunStatus" = FAIL |
| Test Environments | Test Executions | Environnements de test (Chrome, Firefox, iOS...) | "Test Environments" = Chrome |
| Tests count | Test Set, Test Plan | Nombre de tests dans le set/plan | — |
Xray enrichit JQL avec des fonctions custom qui transforment la recherche de tests :
JQL
# 1. Exigences non couvertes par des tests
issueFunction in requirements() AND NOT issueFunction in tested()
# 2. Tests qui n'ont jamais été exécutés
issuetype = Test AND NOT issueFunction in executedTests()
# 3. Tests en échec dans le dernier sprint
issuetype = Test AND "TestRunStatus" = FAIL AND fixVersion = "Sprint-24"
# 4. Exigences dont la couverture est KO
issuetype = Story AND "Requirement Status" = NOK
# 5. Tests liés à un epic spécifique
issueFunction in testingRequirements("epic = PROJ-100")
# 6. Test Executions d'un environnement spécifique
issuetype = "Test Execution" AND "Test Environments" = "Chrome 120"
# 7. Tests Cucumber (BDD) du projet
issuetype = Test AND "Test Type" = Cucumber AND project = PROJ
# 8. Preconditions orphelines (non liées à des tests)
issuetype = "Pre-Condition" AND NOT issueFunction in preconditionTests()
issueFunction in requirements(), tested(), executedTests(), etc. sont spécifiques à Xray. Elles ne fonctionnent que si Xray est installé et activé dans le projet. Elles ne sont pas disponibles dans les "filtres rapides" des boards — uniquement dans les filtres JQL classiques et les gadgets.
Xray utilise les workflows Jira standard pour ses issue types. Ça veut dire que vous pouvez créer un workflow à 15 statuts avec 47 transitions, des validators, des conditions, des post-functions... Mais ne le faites pas.
Voici les workflows recommandés :
Draft : Test en cours de rédaction. Pas encore exécutable.
In Review : Soumis à un peer ou QA Lead pour validation des steps et expected results.
Approved : Prêt à être inclus dans un Test Plan et exécuté.
Deprecated : Obsolète mais conservé pour traçabilité historique.
Transition "Approved" → condition : seuls les rôles "QA Lead" ou "Test Manager" (organisations réglementées)
Simple et efficace. Le statut se met à jour automatiquement quand les Test Runs sont complétés. Pas besoin de complexifier.
Draft : Plan en préparation (sélection des tests, assignation).
Active : Sprint/release en cours, exécutions en cours.
Completed : Toutes les exécutions terminées, métriques finales.
Archived : Conservé pour l'historique, exclu des dashboards actifs.
C'est là que Xray prend tout son sens. Un test management déconnecté de la CI/CD, c'est comme un GPS déconnecté du satellite — il montre une carte, mais pas votre position réelle.
Jenkinsfile
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'mvn test -Dsurefire.reportFormat=xml'
}
}
stage('Import to Xray') {
steps {
step([$class: 'XrayImportBuilder',
endpointName: '/junit',
importFilePath: 'target/surefire-reports/*.xml',
importToSameExecution: 'true',
serverInstance: 'your-xray-instance',
projectKey: 'MYPROJ',
fixVersion: '3.2.0',
testPlanKey: 'MYPROJ-456',
testEnvironments: 'Chrome 120'
])
}
}
}
}
gitlab-ci.yml
test:
stage: test
script:
- pytest --junitxml=report.xml tests/
after_script:
- |
# Authenticate with Xray Cloud
TOKEN=$(curl -s -X POST \
"https://xray.cloud.getxray.app/api/v2/authenticate" \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"$XRAY_CLIENT_ID\",\"client_secret\":\"$XRAY_CLIENT_SECRET\"}")
# Import JUnit results
curl -X POST \
"https://xray.cloud.getxray.app/api/v2/import/execution/junit?projectKey=PROJ&testPlanKey=PROJ-456" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/xml" \
-d @report.xml
artifacts:
reports:
junit: report.xml
github-actions.yml
- name: Import results to Xray
env:
XRAY_CLIENT_ID: ${{ secrets.XRAY_CLIENT_ID }}
XRAY_CLIENT_SECRET: ${{ secrets.XRAY_CLIENT_SECRET }}
run: |
TOKEN=$(curl -s -X POST \
"https://xray.cloud.getxray.app/api/v2/authenticate" \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"$XRAY_CLIENT_ID\",\"client_secret\":\"$XRAY_CLIENT_SECRET\"}")
curl -X POST \
"https://xray.cloud.getxray.app/api/v2/import/execution/junit?projectKey=PROJ" \
-H "Authorization: Bearer $(echo $TOKEN | tr -d '\"')" \
-H "Content-Type: application/xml" \
-d @test-results/junit.xml
| Format | Endpoint API | Frameworks |
|---|---|---|
| JUnit XML | /import/execution/junit |
JUnit, TestNG, pytest, NUnit, Mocha, Jest |
| Cucumber JSON | /import/execution/cucumber |
Cucumber-JVM, CucumberJS, SpecFlow, Behave |
| Robot Framework | /import/execution/robot |
Robot Framework (output.xml) |
| xUnit XML | /import/execution/xunit |
xUnit.net, MSTest |
| Xray JSON | /import/execution |
Format custom Xray (le plus flexible) |
Xray JSON Format
{
"testExecutionKey": "PROJ-789",
"info": {
"summary": "Regression Suite - Build #1234",
"startDate": "2025-12-01T10:00:00+01:00",
"finishDate": "2025-12-01T10:45:00+01:00",
"testPlanKey": "PROJ-456",
"testEnvironments": ["Chrome 120", "Ubuntu 24"]
},
"tests": [
{
"testKey": "PROJ-100",
"status": "PASSED",
"start": "2025-12-01T10:00:00+01:00",
"finish": "2025-12-01T10:02:30+01:00",
"comment": "All assertions passed"
},
{
"testKey": "PROJ-101",
"status": "FAILED",
"start": "2025-12-01T10:02:31+01:00",
"finish": "2025-12-01T10:05:00+01:00",
"comment": "Expected 200, got 500",
"defects": ["PROJ-999"],
"evidences": [
{
"data": "iVBORw0KGgo...(base64)...",
"filename": "screenshot_error.png",
"contentType": "image/png"
}
]
}
]
}
Xray supporte nativement le format Cucumber/Gherkin. Les tests de type "Cucumber" dans Xray contiennent directement le scénario Gherkin. Et voilà ce qui est puissant : vous pouvez exporter les feature files depuis Xray vers votre repo, et importer les résultats d'exécution après le run CI.
Gherkin dans Xray
# Ce scénario est stocké dans un Test Xray de type "Cucumber"
# Issue key: PROJ-200, lié à Story PROJ-50
@PROJ-200
Feature: User Authentication
As a registered user
I want to log in to the application
So that I can access my dashboard
@PROJ-201
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter "user@test.com" as email
And I enter "ValidPass123!" as password
And I click the login button
Then I should be redirected to the dashboard
And I should see "Welcome back" message
@PROJ-202
Scenario Outline: Failed login with invalid credentials
Given I am on the login page
When I enter "<email>" as email
And I enter "<password>" as password
And I click the login button
Then I should see "<error_message>"
Examples:
| email | password | error_message |
| bad@test.com | pass123 | Invalid credentials |
| user@test.com | | Password is required |
| | pass123 | Email is required |
@PROJ-200 est la clé : c'est lui qui fait le lien entre le feature file dans Git et le Test dans Xray. Quand Cucumber exécute ce scénario et que les résultats sont importés, Xray sait automatiquement que c'est le Test PROJ-200 qui a été exécuté. Sans ce tag, l'import crée un nouveau test au lieu de mettre à jour l'existant.
Vous avez maintenant la vision complète de l'architecture Xray : le modèle de données, l'organisation du repository, les workflows, l'intégration CI/CD, et le BDD natif. L'onglet suivant va couvrir la traçabilité et le reporting — la partie qui justifie tout l'investissement aux yeux du management.
La traçabilité, c'est la capacité de répondre à cette question à tout moment : "Pour cette exigence, quels tests existent, quand ont-ils été exécutés, quel est leur résultat, et quels défauts ont été trouvés ?"
C'est simple à énoncer. C'est terriblement difficile à maintenir dans la durée. Et c'est ce que les auditeurs vérifient en premier.
La beauté de ce modèle : la boucle est fermée. D'une exigence, vous remontez au bug. D'un bug, vous retrouvez l'exigence. D'un test, vous voyez tout son historique d'exécution. C'est bidirectionnel et automatique — à condition que les liens soient créés.
Le Coverage Report est probablement la fonctionnalité la plus importante d'Xray. Il vous montre, en un coup d'œil, quelles exigences sont couvertes par des tests, lesquelles ne le sont pas, et quel est le résultat des dernières exécutions.
Pour l'activer :
| KPI | Source | Cible | Alerte si... |
|---|---|---|---|
| Requirement Coverage % | Traceability Report | > 90% | < 70% → exigences non testées |
| Test Execution Pass Rate | Test Plan Board | > 85% | < 70% → qualité en danger |
| Test Execution Progress | Test Plan % executed | 100% avant release | < 80% à J-2 → risque de release |
| Flaky Test Rate | Historique Test Runs | < 5% | > 10% → tests instables à fixer |
| Defect Leakage | Bugs trouvés en prod vs pre-prod | < 10% | > 20% → couverture insuffisante |
| Test Automation Ratio | Tests par Test Type | > 60% | < 30% → automatisation en retard |
| Average Time to Test | Test Execution dates | Variable | Tendance croissante → tests trop complexes |
Un dashboard QA efficace raconte une histoire en 10 secondes. Le manager qui l'ouvre doit comprendre immédiatement : "est-ce qu'on peut releaser ou pas ?" Pas besoin de 15 gadgets — 5 suffisent.
project = PROJ AND issuetype = Story AND fixVersion = currentRelease()Pour les organisations réglementées (banque, pharma, aérospatiale, défense), la traçabilité n'est pas un "nice-to-have" — c'est une obligation légale. L'auditeur veut voir :
Soyons clairs : sans eazyBI (ou Custom Charts), le reporting Xray est basique. Les gadgets natifs Jira couvrent le minimum mais ne permettent pas les analyses croisées, les tendances historiques, ou les drill-downs. eazyBI est le complément quasi-obligatoire pour un reporting digne de ce nom.
Ce qu'eazyBI permet que les gadgets Jira ne font pas :
| Besoin | Gadgets Jira natifs | eazyBI |
|---|---|---|
| Couverture par sprint/release | ⚠️ Limité (un filtre par gadget) | ✅ Drill-down dynamique |
| Tendance PASS/FAIL sur 6 mois | ❌ Pas d'historique | ✅ Time series natif |
| Heatmap par module/composant | ❌ | ✅ Pivot table + heatmap |
| Top flaky tests | ❌ | ✅ Calcul de variance sur les runs |
| Export automatisé PDF | ❌ | ✅ Scheduled reports |
| Corrélation défauts ↔ couverture | ❌ | ✅ Cross-dimension analysis |
eazyBI coûte ~$10/mois pour 10 utilisateurs, ~$50/mois pour 100. Ajoutez-le au budget Xray dès le départ. Un test management sans reporting exploitable, c'est comme avoir une voiture sans tableau de bord — vous roulez, mais vous ne savez pas à quelle vitesse ni combien de carburant il reste.
La traçabilité et le reporting sont ce qui transforme Xray d'un "outil de plus" en un avantage stratégique. Quand le management voit un dashboard clair qui montre la couverture, les tendances, et les risques en temps réel, la QA passe de centre de coût à centre de confiance.
L'onglet final va couvrir le sujet le plus pratique : comment migrer vers Xray depuis votre outil actuel, et comment industrialiser votre setup pour le long terme.
Migrer un test management, c'est comme déménager une bibliothèque. Vous pouvez mettre tous les livres dans des cartons et les balancer dans le camion — mais à l'arrivée, bonne chance pour retrouver quoi que ce soit. Ou vous pouvez planifier, trier, étiqueter, et arriver avec une bibliothèque mieux organisée qu'avant le déménagement.
Ce chapitre couvre les migrations réelles que j'ai observées, avec les outils, les pièges, et les stratégies concrètes.
HP ALM est le mastodonte que beaucoup d'organisations veulent quitter — licences coûteuses, interface vieillissante, impossibilité de travailler sur plusieurs projets simultanément. Mais c'est aussi la migration la plus complexe, parce que le modèle de données ALM est fondamentalement différent de Jira.
1. Xray Test Case Importer (intégré) : Import CSV/Excel avec mapping wizard. Supporte HP ALM/QC v12.5x directement. Pour les versions plus anciennes : export CSV depuis ALM → import via l'importer.
2. ALM Xray Jumper (BRIGHTKNOCK) : Outil tiers spécialisé. Extraction des données via l'UI HP ALM, mapping pré-défini ALM→Xray, import via REST API Jira/Xray. Accès aux données ALM via OTA + tables DB + file repository. C'est l'option "industrielle" pour les gros volumes.
3. Scripts custom : Pour les cas complexes, un script Python/Java qui extrait via l'API ALM OTA et injecte via l'API REST Xray. Plus de contrôle, mais plus de travail.
| HP ALM | Xray / Jira | Notes |
|---|---|---|
| Requirements | Story / Epic | Créer les requirements comme issues Jira d'abord |
| Test (dans Test Plan) | Test (issue type) | Mapping direct des steps |
| Test Set | Test Set ou Test Plan | Selon l'usage (regroupement logique vs campagne) |
| Test Instance (dans Test Lab) | Test Execution | ⚠️ L'historique des runs est le plus dur à migrer |
| Run | Test Run | Statuts à remapper (Passed→PASS, Failed→FAIL...) |
| Defect | Bug (Jira) | Recréer les liens Test Run → Bug |
| Test Folders | Test Repository (dossiers) | Structure arborescente à recréer |
| Attachments | Jira Attachments | ⚠️ Migration séparée via API REST |
Migration HP QC → Xray/Jira. 58 000 scénarios, 200 000 steps, équipes réparties sur 3 pays. Timeline : 3-4 mois incluant la phase pilot, le déploiement, la formation interne, et la migration elle-même.
Ce qui a bien marché : pas d'impact sur les performances Jira, unification des équipes dev/test, création de défauts pendant l'exécution avec auto-linking, travail multi-projets simultané (impossible dans HP QC), audits facilités.
Ce qui était dur : l'historique d'exécution (partiellement migré), les custom fields ALM (nécessitant un remapping complet), les pièces jointes volumineuses (migration séparée en batch).
TestLink est open source et populaire dans les PME. La migration est plus simple car le modèle de données est plus léger.
Étape 1 : Export depuis TestLink au format XML
Étape 2 : Utiliser les scripts de conversion disponibles sur le GitHub Xray pour convertir XML → CSV
Étape 3 : Import CSV via le Test Case Importer Xray avec les fichiers de configuration JSON
Étape 4 : Validation post-import (vérifier steps, preconditions, liens)
| TestLink | Xray | Notes |
|---|---|---|
| Test Suite | Test Set / Dossier Repository | Au choix selon la structure souhaitée |
| Test Case | Test | Mapping direct |
| Preconditions | Pre-Condition (issue type) | Créées séparément et liées |
| Test Steps | Test Steps | Mapping direct (action, expected result) |
| Custom Fields | Custom Fields Jira | ⚠️ Mapping manuel requis |
Ironie du sort : beaucoup d'équipes migrent de Zephyr vers Xray tout en restant dans Jira. La raison ? L'intégration "native" d'Xray (issue types) vs les panels custom de Zephyr.
1. Export des tests Zephyr en Excel/XML depuis l'interface Zephyr
2. Conversion du format via script (les custom step fields Zephyr ne sont pas directement supportés dans Xray — mapping vers "Additional step information")
3. Import via Test Case Importer avec le wizard de field mapping
4. Recréation manuelle des liens Test↔Requirement (les plus critiques)
5. Validation : comparer les comptages (nombre de tests, steps, liens) entre ancien et nouveau
Le cas le plus fréquent. Et paradoxalement, le plus risqué — parce que les fichiers Excel sont rarement structurés de manière cohérente.
Le Test Case Importer attend un CSV avec des colonnes spécifiques. Voici la structure recommandée :
CSV
"Test ID","Summary","Description","Priority","Labels","Step #","Action","Data","Expected Result"
"TC-001","Login avec identifiants valides","Vérifier le login standard","High","smoke,regression","1","Ouvrir la page de login","URL: /login","Page de login affichée"
"TC-001","","","","","2","Entrer email et mot de passe","user@test.com / Pass123","Champs remplis"
"TC-001","","","","","3","Cliquer sur 'Connexion'","","Redirection vers dashboard"
"TC-002","Login avec mot de passe invalide","Vérifier le rejet","High","smoke","1","Ouvrir la page de login","URL: /login","Page de login affichée"
"TC-002","","","","","2","Entrer email et mauvais mdp","user@test.com / bad","Message d'erreur affiché"
Règles critiques :
— Un test sur plusieurs lignes : la première ligne contient le Summary/Priority/Labels, les suivantes seulement les steps
— Séparez les CSV par type de test (Manual, Cucumber, Generic)
— Nettoyez les données AVANT l'import : supprimez les tests obsolètes, uniformisez les labels
— Utilisez un fichier de configuration JSON pour rendre l'import reproductible
TestRail est un outil solide, mais externe à Jira. La migration vers Xray vise à tout centraliser.
1. Export depuis TestRail : Sections + Test Cases en CSV via l'interface d'admin
2. Attention aux templates TestRail : Step-by-Step, Plain Text, BDD-Gherkin — chacun a un format CSV différent
3. Import CSV via Test Case Importer Xray
4. Les custom fields TestRail doivent être recréés dans Jira puis mappés
5. Les test results historiques : export via API TestRail → import via API Xray (script custom nécessaire)
Le niveau ultime d'intégration, c'est le "Test as Code" : vos tests vivent dans Git, sont exécutés par la CI, et les résultats remontent automatiquement dans Xray. Le test management devient une extension naturelle du cycle de développement, pas un process parallèle.
Python
# Script d'export des feature files depuis Xray Cloud
# À intégrer dans votre pipeline CI ou un cron job
import requests
import zipfile
import io
import os
XRAY_BASE = "https://xray.cloud.getxray.app/api/v2"
def authenticate():
resp = requests.post(f"{XRAY_BASE}/authenticate", json={
"client_id": os.environ["XRAY_CLIENT_ID"],
"client_secret": os.environ["XRAY_CLIENT_SECRET"]
})
return resp.json() # Returns the token string
def export_features(token, test_keys):
"""Export feature files from Xray for given test keys"""
headers = {"Authorization": f"Bearer {token}"}
# Export Cucumber features for specific tests
resp = requests.get(
f"{XRAY_BASE}/export/cucumber",
params={"keys": ";".join(test_keys)},
headers=headers
)
if resp.status_code == 200:
# Response is a zip file containing .feature files
z = zipfile.ZipFile(io.BytesIO(resp.content))
z.extractall("src/test/resources/features/")
print(f"✅ Exported {len(z.namelist())} feature files")
for name in z.namelist():
print(f" → {name}")
else:
print(f"❌ Export failed: {resp.status_code}")
if __name__ == "__main__":
token = authenticate()
# Export all Cucumber tests from project
export_features(token, ["PROJ-200", "PROJ-201", "PROJ-202"])
C'est le conseil le plus contre-intuitif de cette formation : le jour où vous adoptez Xray, préparez votre plan de sortie. Pas parce que vous allez partir — mais parce que savoir que vous pouvez partir vous rend libre dans vos choix technologiques.
| Phase | Action | Critère de succès |
|---|---|---|
| Avant | Évaluer la maturité actuelle (niveau 1-5) | Document d'évaluation partagé |
| Avant | Choisir l'édition (Standard vs Enterprise) | Décision argumentée, budget validé |
| Setup | Configurer les issue types, workflows, repository | Structure validée par l'équipe QA + PO |
| Setup | Activer Requirement Coverage + mapping | Champ "Requirement Status" visible sur les boards |
| Migration | Pilot migration (1 projet, ~100 tests) | Tests importés, liens vérifiés, exécution validée |
| Migration | Migration progressive des autres projets | Comptages validés, mapping documenté |
| Intégration | Connecter CI/CD → Xray (JUnit/Cucumber import) | Résultats auto remontent en temps réel |
| Reporting | Créer le dashboard QA (5 gadgets essentiels) | Dashboard présenté en sprint review |
| Long terme | Test Hygiene Review trimestrielle | < 20% de tests non exécutés en 6 mois |
| Long terme | Backup automatisé + documentation exit | Export mensuel vérifié, modèle documenté |
La différence entre un setup Xray qui marche et un qui échoue n'est pas l'outil — c'est la rigueur de la mise en œuvre.
L'API est le vrai moteur d'Xray. Sans elle, vous avez un outil de saisie manuelle. Avec elle, vous avez une plateforme d'orchestration QA programmable. Tout ce qui est possible dans l'interface est faisable — et souvent mieux — via l'API.
Ce chapitre est votre cheat sheet : les endpoints, les schémas de données, les exemples prêts à copier dans vos scripts et pipelines.
| Aspect | Xray Cloud | Xray Data Center / Server |
|---|---|---|
| Base URL | https://xray.cloud.getxray.app/api/v2 |
https://your-jira.com/rest/raven/2.0/api |
| Auth | OAuth2 — Client ID + Client Secret → Bearer Token | PAT (Personal Access Token) ou Basic Auth |
| GraphQL | ✅ Oui (/api/v2/graphql) |
❌ Non |
| REST | ✅ v2 | ✅ v1 + v2 |
| Rate Limits | Oui (dépend de l'édition — Standard < Advanced < Enterprise) | Limité par l'infra serveur |
| Webhooks | ✅ Via Jira Automation + API | ✅ Via Jira Webhooks |
Bash
# 1. Obtenir le token (valide ~1h)
TOKEN=$(curl -s -X POST \
"https://xray.cloud.getxray.app/api/v2/authenticate" \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}')
# Le token est retourné directement comme string (avec les guillemets)
echo "Token: $TOKEN"
# 2. Utiliser le token dans les appels suivants
curl -X GET "https://xray.cloud.getxray.app/api/v2/..." \
-H "Authorization: Bearer $(echo $TOKEN | tr -d '\"')"
Python
import requests
import os
class XrayCloudClient:
"""Client Xray Cloud réutilisable avec gestion du token."""
BASE_URL = "https://xray.cloud.getxray.app/api/v2"
def __init__(self):
self.client_id = os.environ["XRAY_CLIENT_ID"]
self.client_secret = os.environ["XRAY_CLIENT_SECRET"]
self.token = None
def authenticate(self):
"""Obtenir un Bearer token OAuth2."""
resp = requests.post(
f"{self.BASE_URL}/authenticate",
json={
"client_id": self.client_id,
"client_secret": self.client_secret
}
)
resp.raise_for_status()
self.token = resp.json() # String directe
return self.token
@property
def headers(self):
if not self.token:
self.authenticate()
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def get(self, endpoint, **kwargs):
return requests.get(
f"{self.BASE_URL}{endpoint}",
headers=self.headers, **kwargs
)
def post(self, endpoint, **kwargs):
return requests.post(
f"{self.BASE_URL}{endpoint}",
headers=self.headers, **kwargs
)
# Usage
xray = XrayCloudClient()
xray.authenticate()
print("✅ Connecté à Xray Cloud")
Bash
# Option 1 : Personal Access Token (recommandé, Jira DC >= 8.14)
curl -X GET "https://your-jira.com/rest/raven/2.0/api/test/PROJ-100" \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN"
# Option 2 : Basic Auth (legacy)
curl -X GET "https://your-jira.com/rest/raven/2.0/api/test/PROJ-100" \
-u "username:password"
XRAY_CLIENT_ID, XRAY_CLIENT_SECRET), des Vault (HashiCorp Vault, AWS Secrets Manager), ou les secrets de votre CI (Jenkins Credentials, GitLab CI Variables).
/api/v2/├── authenticate .................... POST → Bearer Token├── graphql ......................... POST → Requêtes GraphQL│├── import/│ ├── execution .................. POST → Import résultats (Xray JSON)│ ├── execution/junit ............ POST → Import JUnit XML│ ├── execution/testng ........... POST → Import TestNG XML│ ├── execution/nunit ............ POST → Import NUnit XML│ ├── execution/xunit ............ POST → Import xUnit XML│ ├── execution/robot ............ POST → Import Robot Framework│ ├── execution/cucumber ......... POST → Import Cucumber JSON│ ├── execution/behave ........... POST → Import Behave JSON│ └── execution/bundle ........... POST → Import multi-format (zip)│├── export/│ ├── cucumber ................... GET → Export .feature files (zip)│ └── test ....................... GET → Export test definitions│├── test/│ ├── {testKey} .................. GET → Détail d'un test│ ├── {testKey}/steps ............ GET → Steps d'un test│ ├── {testKey}/preconditions .... GET → Preconditions liées│ └── {testKey}/testruns ......... GET → Historique des runs│├── testplan/│ ├── {testPlanKey} .............. GET → Détail du plan│ └── {testPlanKey}/test ......... GET → Tests du plan│├── testexec/│ ├── {testExecKey} .............. GET → Détail de l'exécution│ └── {testExecKey}/test ......... GET → Tests dans l'exécution│├── testrun/│ ├── {testRunId} ................ GET → Détail d'un run│ ├── {testRunId}/status ......... PUT → Modifier le statut│ ├── {testRunId}/comment ........ PUT → Ajouter un commentaire│ ├── {testRunId}/defect ......... POST → Lier un défaut│ └── {testRunId}/attachment ..... POST → Ajouter une evidence│└── testset/ └── {testSetKey}/test .......... GET → Tests du set
C'est l'endpoint le plus utilisé. Chaque pipeline CI appelle ça.
Bash
# Import basique — crée une Test Execution automatiquement
curl -X POST \
"https://xray.cloud.getxray.app/api/v2/import/execution/junit?projectKey=PROJ" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/xml" \
-d @target/surefire-reports/TEST-com.example.LoginTest.xml
# Import avec contexte enrichi
curl -X POST \
"https://xray.cloud.getxray.app/api/v2/import/execution/junit?\
projectKey=PROJ&\
testPlanKey=PROJ-456&\
testEnvironments=Chrome%20120;Ubuntu%2024&\
fixVersion=3.2.0&\
revision=abc123def" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/xml" \
-d @report.xml
Paramètres query :
| Param | Type | Description |
|---|---|---|
projectKey | String (requis) | Clé du projet Jira |
testPlanKey | String | Lier l'exécution à un Test Plan |
testExecKey | String | Mettre à jour une Test Execution existante (au lieu d'en créer une nouvelle) |
testEnvironments | String (séparés par ;) | Environnements de test |
fixVersion | String | Version Jira associée |
revision | String | Hash du commit / révision |
Réponse :
JSON
{
"testExecIssue": {
"id": "12345",
"key": "PROJ-789",
"self": "https://your-jira.atlassian.net/rest/api/2/issue/12345"
}
}
testExecKey pour regrouper les résultats de plusieurs jobs CI dans la même Test Execution. Sans ce paramètre, chaque appel crée une nouvelle Test Execution — ce qui pollue votre historique.
Bash
curl -X POST \
"https://xray.cloud.getxray.app/api/v2/import/execution/cucumber" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @target/cucumber-report.json
@PROJ-200 dans le feature file fait le mapping automatique avec le Test PROJ-200 dans Xray. Sans tag, Xray crée un nouveau test.
Python
# Import avec le format Xray JSON — contrôle total
import requests
import json
import base64
def import_xray_results(xray_client, results):
"""
Import des résultats au format Xray JSON.
Permet : evidence en base64, defects liés, commentaires,
timestamps précis, infos enrichies.
"""
payload = {
"testExecutionKey": "PROJ-789", # ou omis pour auto-création
"info": {
"project": "PROJ",
"summary": f"Regression Suite - Build #{os.environ.get('BUILD_NUMBER', 'local')}",
"description": "Exécution automatisée via pipeline CI",
"startDate": "2026-02-10T09:00:00+01:00",
"finishDate": "2026-02-10T09:45:00+01:00",
"testPlanKey": "PROJ-456",
"testEnvironments": ["Chrome 121", "Ubuntu 24.04"],
"version": "3.2.0",
"revision": "a1b2c3d4e5f6"
},
"tests": []
}
for test in results:
test_entry = {
"testKey": test["key"],
"status": test["status"], # PASSED, FAILED, TODO, EXECUTING, ABORTED
"start": test["start_time"],
"finish": test["end_time"],
"comment": test.get("comment", ""),
}
# Ajouter des défauts liés si le test a échoué
if test["status"] == "FAILED" and test.get("defect_key"):
test_entry["defects"] = [test["defect_key"]]
# Ajouter des preuves (screenshots) en base64
if test.get("screenshot_path"):
with open(test["screenshot_path"], "rb") as img:
b64 = base64.b64encode(img.read()).decode()
test_entry["evidences"] = [{
"data": b64,
"filename": f"evidence_{test['key']}.png",
"contentType": "image/png"
}]
# Ajouter les résultats step par step
if test.get("steps"):
test_entry["steps"] = [
{
"status": step["status"],
"comment": step.get("comment", ""),
"actualResult": step.get("actual_result", "")
}
for step in test["steps"]
]
payload["tests"].append(test_entry)
resp = xray_client.post("/import/execution", json=payload)
resp.raise_for_status()
result = resp.json()
print(f"✅ Import OK → Test Execution: {result['testExecIssue']['key']}")
return result
# Exemple d'appel
results = [
{
"key": "PROJ-100",
"status": "PASSED",
"start_time": "2026-02-10T09:00:00+01:00",
"end_time": "2026-02-10T09:02:30+01:00",
"comment": "All 5 assertions passed"
},
{
"key": "PROJ-101",
"status": "FAILED",
"start_time": "2026-02-10T09:02:31+01:00",
"end_time": "2026-02-10T09:05:00+01:00",
"comment": "Expected HTTP 200, got 500 on /api/users",
"defect_key": "PROJ-999",
"screenshot_path": "/tmp/screenshots/error_PROJ-101.png",
"steps": [
{"status": "PASSED", "actual_result": "Login OK"},
{"status": "PASSED", "actual_result": "Navigation OK"},
{"status": "FAILED", "actual_result": "HTTP 500 Internal Server Error",
"comment": "API /users returned 500 since build #1233"}
]
}
]
xray = XrayCloudClient()
xray.authenticate()
import_xray_results(xray, results)
Bash
# Export par clés de test spécifiques
curl -X GET \
"https://xray.cloud.getxray.app/api/v2/export/cucumber?keys=PROJ-200;PROJ-201;PROJ-202" \
-H "Authorization: Bearer $TOKEN" \
-o features.zip
unzip features.zip -d src/test/resources/features/
# Export par filtre JQL
curl -X GET \
"https://xray.cloud.getxray.app/api/v2/export/cucumber?\
filter=12345" \
-H "Authorization: Bearer $TOKEN" \
-o features.zip
Paramètres :
| Param | Description |
|---|---|
keys | Clés de tests séparées par ; |
filter | ID d'un filtre JQL sauvegardé |
Réponse : Fichier ZIP contenant les .feature avec les tags @PROJ-XXX automatiques.
GraphQL est la voie royale pour interroger Xray Cloud. Une seule requête peut récupérer les tests, leurs steps, leurs préconditions, et leur historique d'exécution — là où REST nécessiterait 4 appels séparés.
GraphQL
# POST https://xray.cloud.getxray.app/api/v2/graphql
{
getTests(jql: "project = PROJ AND issuetype = Test", limit: 50) {
total
start
limit
results {
issueId
jira(fields: ["key", "summary", "status", "priority", "labels"])
testType { name kind }
folder { path name }
steps {
id
action
data
result
attachments { id filename }
}
preconditions(limit: 10) {
results {
issueId
jira(fields: ["key", "summary"])
definition
preconditionType { name }
}
}
testRuns(limit: 5) {
results {
id
status { name color }
startedOn
finishedOn
executedById
testExecution { issueId jira(fields: ["key"]) }
evidence { filename }
defects { issueId jira(fields: ["key", "summary", "status"]) }
comment
}
}
}
}
}
GraphQL
{
getTestPlan(issueId: "12345") {
issueId
jira(fields: ["key", "summary"])
tests(limit: 100) {
total
results {
issueId
jira(fields: ["key", "summary"])
testType { name }
lastTestRunStatus
}
}
testExecutions(limit: 20) {
results {
issueId
jira(fields: ["key", "summary", "status"])
testEnvironments
testRuns(limit: 100) {
results {
status { name }
startedOn
finishedOn
}
}
}
}
}
}
GraphQL
{
getTests(jql: "issuetype = Test AND project = PROJ", limit: 100) {
results {
issueId
jira(fields: ["key", "summary"])
testType { name }
# Les requirements couverts par ce test
requirements(limit: 10) {
results {
issueId
jira(fields: ["key", "summary", "status"])
}
}
lastTestRunStatus
}
}
}
Python
def graphql_query(xray_client, query, variables=None):
"""Exécuter une requête GraphQL sur Xray Cloud."""
payload = {"query": query}
if variables:
payload["variables"] = variables
resp = xray_client.post("/graphql", json=payload)
resp.raise_for_status()
data = resp.json()
if "errors" in data:
for err in data["errors"]:
print(f"⚠️ GraphQL Error: {err['message']}")
return data.get("data", {})
# Exemple : Dashboard de couverture en une requête
COVERAGE_QUERY = """
{
getTests(jql: "project = PROJ AND issuetype = Test", limit: 200) {
total
results {
issueId
jira(fields: ["key", "summary"])
testType { name }
lastTestRunStatus
requirements(limit: 5) {
results {
jira(fields: ["key", "summary"])
}
}
}
}
}
"""
xray = XrayCloudClient()
xray.authenticate()
data = graphql_query(xray, COVERAGE_QUERY)
tests = data["getTests"]["results"]
total = data["getTests"]["total"]
# Calculer les stats
passed = sum(1 for t in tests if t.get("lastTestRunStatus") == "PASSED")
failed = sum(1 for t in tests if t.get("lastTestRunStatus") == "FAILED")
no_run = sum(1 for t in tests if not t.get("lastTestRunStatus"))
orphans = sum(1 for t in tests if not t.get("requirements", {}).get("results"))
print(f"📊 Dashboard Xray — {total} tests")
print(f" ✅ Passed: {passed} ({passed/total*100:.0f}%)")
print(f" ❌ Failed: {failed} ({failed/total*100:.0f}%)")
print(f" ⬜ No runs: {no_run} ({no_run/total*100:.0f}%)")
print(f" 🔗 Orphans: {orphans} ({orphans/total*100:.0f}%) ← sans requirement")
Python
class TestRunManager:
"""Gestion avancée des Test Runs via API Xray."""
def __init__(self, xray_client):
self.xray = xray_client
def update_status(self, test_run_id, status, comment=None):
"""
Modifier le statut d'un Test Run.
Statuts possibles : PASSED, FAILED, TODO, EXECUTING, ABORTED
"""
payload = {"status": status}
if comment:
payload["comment"] = comment
resp = self.xray.put(f"/testrun/{test_run_id}/status", json=payload)
resp.raise_for_status()
print(f"✅ Test Run {test_run_id} → {status}")
def add_evidence(self, test_run_id, filepath, content_type="image/png"):
"""Attacher une evidence (screenshot, log) au Test Run."""
with open(filepath, "rb") as f:
files = {"file": (os.path.basename(filepath), f, content_type)}
resp = requests.post(
f"{self.xray.BASE_URL}/testrun/{test_run_id}/attachment",
headers={"Authorization": f"Bearer {self.xray.token}"},
files=files
)
resp.raise_for_status()
print(f"📎 Evidence ajoutée au Test Run {test_run_id}")
def link_defect(self, test_run_id, defect_key):
"""Lier un défaut Jira à un Test Run."""
resp = self.xray.post(
f"/testrun/{test_run_id}/defect",
json={"issueKey": defect_key}
)
resp.raise_for_status()
print(f"🐛 Defect {defect_key} lié au Test Run {test_run_id}")
def add_comment(self, test_run_id, comment):
"""Ajouter un commentaire au Test Run."""
resp = self.xray.put(
f"/testrun/{test_run_id}/comment",
json={"comment": comment}
)
resp.raise_for_status()
# Usage dans un framework de test custom
runner = TestRunManager(xray)
# Après un test qui échoue
runner.update_status("tr-12345", "FAILED", "Timeout on API call after 30s")
runner.add_evidence("tr-12345", "/tmp/screenshots/api_timeout.png")
runner.link_defect("tr-12345", "PROJ-999")
runner.add_comment("tr-12345", "Reproduced consistently since build #1233")
Les webhooks permettent de déclencher des actions automatiques quand quelque chose se passe dans Xray. Un test échoue ? Envoyez une notif Slack. Une Test Execution est terminée ? Déclenchez un rapport. Un défaut est créé depuis un Test Run ? Assignez-le automatiquement.
Python
"""
Listener Xray — Serveur webhook qui réagit aux événements Jira/Xray.
Configure un webhook Jira pointant vers ce serveur.
Jira Admin → System → Webhooks → Create
URL: https://your-server.com/webhook/xray
Events: Issue created, Issue updated
JQL Filter: issuetype in (Test, "Test Execution", Bug)
"""
from flask import Flask, request, jsonify
import requests
import json
from datetime import datetime
app = Flask(__name__)
# Configuration
SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"
XRAY_EVENTS_LOG = "xray_events.log"
def log_event(event_type, data):
"""Logger tous les événements pour audit."""
with open(XRAY_EVENTS_LOG, "a") as f:
f.write(json.dumps({
"timestamp": datetime.now().isoformat(),
"type": event_type,
"data": data
}) + "\n")
def notify_slack(message, color="#FF5630"):
"""Envoyer une notification Slack."""
requests.post(SLACK_WEBHOOK, json={
"attachments": [{
"color": color,
"text": message,
"footer": "Xray Listener",
"ts": int(datetime.now().timestamp())
}]
})
@app.route("/webhook/xray", methods=["POST"])
def xray_webhook():
"""Point d'entrée principal du webhook."""
payload = request.json
event = payload.get("webhookEvent", "unknown")
issue = payload.get("issue", {})
issue_type = issue.get("fields", {}).get("issuetype", {}).get("name", "")
issue_key = issue.get("key", "N/A")
summary = issue.get("fields", {}).get("summary", "")
log_event(event, {"key": issue_key, "type": issue_type})
# === ÉVÉNEMENT 1 : Test Execution créée ===
if event == "jira:issue_created" and issue_type == "Test Execution":
assignee = issue.get("fields", {}).get("assignee", {}).get("displayName", "Non assigné")
notify_slack(
f"▶️ Nouvelle Test Execution : *{issue_key}* — {summary}\n"
f"Assigné à : {assignee}",
color="#0052CC"
)
# === ÉVÉNEMENT 2 : Bug créé depuis un Test Run ===
elif event == "jira:issue_created" and issue_type == "Bug":
priority = issue.get("fields", {}).get("priority", {}).get("name", "?")
# Vérifier si le bug est lié à un test (via issuelinks)
links = issue.get("fields", {}).get("issuelinks", [])
test_links = [
l for l in links
if l.get("type", {}).get("name") == "is blocked by"
]
if test_links:
test_key = test_links[0].get("outwardIssue", {}).get("key", "?")
notify_slack(
f"🐛 Bug créé depuis un test !\n"
f"Bug : *{issue_key}* ({priority}) — {summary}\n"
f"Test source : {test_key}",
color="#DE350B"
)
# === ÉVÉNEMENT 3 : Test Execution terminée ===
elif event == "jira:issue_updated" and issue_type == "Test Execution":
new_status = payload.get("changelog", {}).get("items", [{}])
for change in payload.get("changelog", {}).get("items", []):
if change.get("field") == "status" and change.get("toString") == "Done":
notify_slack(
f"✅ Test Execution terminée : *{issue_key}* — {summary}\n"
f"📊 Résultats disponibles dans Xray",
color="#36B37E"
)
# === ÉVÉNEMENT 4 : Test passé en status Deprecated ===
elif event == "jira:issue_updated" and issue_type == "Test":
for change in payload.get("changelog", {}).get("items", []):
if change.get("field") == "status" and change.get("toString") == "Deprecated":
notify_slack(
f"🗄️ Test archivé : *{issue_key}* — {summary}\n"
f"Vérifiez qu'il est retiré des Test Plans actifs.",
color="#97A0AF"
)
return jsonify({"status": "ok"}), 200
@app.route("/webhook/ci-trigger", methods=["POST"])
def ci_trigger():
"""
Endpoint pour déclencher un pipeline CI
quand un Test Plan passe en status 'Active'.
"""
payload = request.json
issue = payload.get("issue", {})
issue_type = issue.get("fields", {}).get("issuetype", {}).get("name", "")
if issue_type == "Test Plan":
for change in payload.get("changelog", {}).get("items", []):
if change.get("field") == "status" and change.get("toString") == "Active":
test_plan_key = issue.get("key")
# Déclencher le pipeline Jenkins
requests.post(
"https://jenkins.example.com/job/regression-suite/build",
auth=("jenkins_user", "jenkins_token"),
json={"parameter": [
{"name": "TEST_PLAN_KEY", "value": test_plan_key}
]}
)
notify_slack(
f"🚀 Pipeline déclenché pour Test Plan *{test_plan_key}*",
color="#0052CC"
)
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
https://your-server.com/webhook/xrayissuetype in (Test, "Test Execution", "Test Plan", Bug) AND project = PROJSi vous ne voulez pas maintenir un serveur webhook, Jira Automation (natif dans Jira Cloud) peut gérer beaucoup de cas automatiquement :
Jira Automation Rules
# Règle 1 : Notifier Slack quand un test échoue
TRIGGER: Issue updated
CONDITION: Issue type = "Test Execution" AND Status changed to "Done"
ACTION: Send Slack message → "#qa-alerts"
Message: "Test Execution {{issue.key}} terminée — vérifiez les résultats"
# Règle 2 : Assigner les bugs auto-créés au tech lead
TRIGGER: Issue created
CONDITION: Issue type = Bug AND Issue has link type "is created by"
ACTION: Assign to → "tech.lead@company.com"
# Règle 3 : Ajouter un commentaire quand la couverture est OK
TRIGGER: Field value changed
CONDITION: Field "Requirement Status" = OK
ACTION: Add comment → "✅ Couverture validée — tous les tests passent"
# Règle 4 : Transition auto du Test Plan quand toutes les exécutions sont Done
TRIGGER: Issue updated (Test Execution → Done)
CONDITION: Related Test Plan has all Test Executions in Done
ACTION: Transition Test Plan → "Completed"
Python
"""
Script de rapport quotidien de couverture Xray.
À exécuter via cron ou CI schedule.
Envoie un résumé Slack + génère un JSON pour dashboard.
"""
import requests
import json
from datetime import datetime
def generate_coverage_report(xray_client, project_key, version=None):
"""Générer un rapport de couverture complet."""
# 1. Récupérer les tests et leur statut via GraphQL
jql = f"project = {project_key} AND issuetype = Test"
if version:
jql += f" AND fixVersion = '{version}'"
query = """
query($jql: String!) {
getTests(jql: $jql, limit: 500) {
total
results {
issueId
jira(fields: ["key", "summary", "labels"])
testType { name }
lastTestRunStatus
requirements(limit: 5) {
total
results { jira(fields: ["key"]) }
}
}
}
}
"""
data = graphql_query(xray_client, query, {"jql": jql})
tests = data.get("getTests", {}).get("results", [])
total = data.get("getTests", {}).get("total", 0)
# 2. Calculer les métriques
stats = {
"date": datetime.now().isoformat(),
"project": project_key,
"version": version,
"total_tests": total,
"by_status": {},
"by_type": {},
"orphan_tests": 0, # Tests sans requirement
"coverage_rate": 0
}
for test in tests:
# Par statut
status = test.get("lastTestRunStatus") or "NO_RUN"
stats["by_status"][status] = stats["by_status"].get(status, 0) + 1
# Par type
test_type = test.get("testType", {}).get("name", "Unknown")
stats["by_type"][test_type] = stats["by_type"].get(test_type, 0) + 1
# Orphelins
req_count = test.get("requirements", {}).get("total", 0)
if req_count == 0:
stats["orphan_tests"] += 1
# Taux de couverture = tests liés à au moins 1 requirement
linked = total - stats["orphan_tests"]
stats["coverage_rate"] = round((linked / total * 100), 1) if total > 0 else 0
# Pass rate
passed = stats["by_status"].get("PASSED", 0)
executed = total - stats["by_status"].get("NO_RUN", 0)
stats["pass_rate"] = round((passed / executed * 100), 1) if executed > 0 else 0
# 3. Générer le message Slack
status_emoji = "✅" if stats["pass_rate"] > 85 else "⚠️" if stats["pass_rate"] > 70 else "🔴"
slack_message = f"""
{status_emoji} *Rapport QA Quotidien — {project_key}*
📅 {datetime.now().strftime('%d/%m/%Y %H:%M')}
{'Version: ' + version if version else ''}
📊 *Métriques :*
• Tests total : {total}
• ✅ Passed : {stats['by_status'].get('PASSED', 0)}
• ❌ Failed : {stats['by_status'].get('FAILED', 0)}
• ⬜ Non exécutés : {stats['by_status'].get('NO_RUN', 0)}
• 🔗 Taux de couverture : {stats['coverage_rate']}%
• 📈 Pass rate : {stats['pass_rate']}%
• 👻 Tests orphelins : {stats['orphan_tests']}
🧪 *Par type :*
{chr(10).join(f"• {k}: {v}" for k, v in stats['by_type'].items())}
"""
return stats, slack_message
# Exécution
xray = XrayCloudClient()
xray.authenticate()
stats, message = generate_coverage_report(xray, "PROJ", "3.2.0")
# Envoyer sur Slack
requests.post(SLACK_WEBHOOK, json={"text": message})
# Sauvegarder le JSON pour historique
with open(f"reports/coverage_{datetime.now().strftime('%Y%m%d')}.json", "w") as f:
json.dump(stats, f, indent=2)
print(f"✅ Rapport généré — Pass rate: {stats['pass_rate']}%")
Python
"""
Script de nettoyage : identifier et labelliser les tests orphelins.
Orphelin = test sans requirement lié ET sans exécution depuis 6 mois.
"""
from datetime import datetime, timedelta
def find_orphan_tests(xray_client, project_key, months_inactive=6):
"""Trouver les tests orphelins et inactifs."""
cutoff = (datetime.now() - timedelta(days=months_inactive * 30)).strftime("%Y-%m-%d")
query = """
query($jql: String!) {
getTests(jql: $jql, limit: 200) {
total
results {
issueId
jira(fields: ["key", "summary", "created", "updated", "labels"])
testType { name }
requirements(limit: 1) { total }
testRuns(limit: 1) {
results {
startedOn
status { name }
}
}
}
}
}
"""
jql = f"project = {project_key} AND issuetype = Test AND updated <= '{cutoff}'"
data = graphql_query(xray_client, query, {"jql": jql})
orphans = []
for test in data.get("getTests", {}).get("results", []):
has_requirements = test.get("requirements", {}).get("total", 0) > 0
has_recent_runs = len(test.get("testRuns", {}).get("results", [])) > 0
if not has_requirements and not has_recent_runs:
key = test["jira"]["key"]
summary = test["jira"]["summary"]
orphans.append({"key": key, "summary": summary})
print(f" 👻 {key}: {summary}")
print(f"\n📊 {len(orphans)} tests orphelins sur {data['getTests']['total']} inactifs")
return orphans
# Pour chaque orphelin, ajouter le label DEPRECATED via API Jira
def label_orphans(orphans, jira_base_url, jira_auth):
"""Ajouter le label DEPRECATED aux tests orphelins."""
for test in orphans:
requests.put(
f"{jira_base_url}/rest/api/2/issue/{test['key']}",
auth=jira_auth,
json={"update": {"labels": [{"add": "DEPRECATED"}]}}
)
print(f" 🏷️ {test['key']} → DEPRECATED")
orphans = find_orphan_tests(xray, "PROJ", months_inactive=6)
# label_orphans(orphans, "https://your-jira.atlassian.net", ("email", "api_token"))
1. DEV push code └→ Git trigger → CI Pipeline start2. CI exécute les tests ├→ Unit tests → JUnit XML ├→ Integration tests → JUnit XML └→ BDD tests → Cucumber JSON3. CI push résultats vers Xray ├→ POST /import/execution/junit (unit + integration) └→ POST /import/execution/cucumber (BDD)4. Xray traite les résultats ├→ Crée/met à jour Test Execution ├→ Met à jour les Test Runs (PASS/FAIL) ├→ Recalcule Requirement Status └→ Met à jour le Test Plan coverage5. Webhooks se déclenchent ├→ Test Execution updated → Listener Flask ├→ Bug créé (si FAIL) → Slack notification └→ Coverage recalculée → Dashboard mis à jour6. Cron quotidien └→ Script rapport couverture → Slack + JSON archive7. Sprint Review └→ PO/Manager consulte le dashboard Jira → Décision Go/No-Go
| Erreur | Cause probable | Solution |
|---|---|---|
401 Unauthorized |
Token expiré (durée ~1h) ou credentials invalides | Ré-authentifiez. Vérifiez client_id/secret. En DC : vérifiez le PAT. |
400 Bad Request |
Format XML/JSON invalide, champ manquant | Validez votre XML JUnit avec un linter. Vérifiez les champs requis. |
404 Not Found |
Issue key invalide, projet inexistant, endpoint incorrect | Vérifiez que le projet existe et que Xray est activé dessus. |
403 Forbidden |
Permissions insuffisantes sur le projet | Vérifiez les permissions Jira du compte API (Project Role, Browse, Create Issue). |
429 Too Many Requests |
Rate limit atteint (Standard < Advanced < Enterprise) | Ajoutez un retry avec backoff exponentiel. Ou upgradez l'édition. |
| Test créé en double | Tag @PROJ-XXX manquant dans le feature Cucumber |
Ajoutez le tag Jira dans le .feature. Sans tag = nouveau test à chaque import. |
| Evidence non attachée | Content-Type incorrect ou fichier trop volumineux | Vérifiez multipart/form-data. Limite : ~10MB par attachment. |
Python
import time
def api_call_with_retry(func, max_retries=3, base_delay=2):
"""Wrapper de retry avec backoff exponentiel pour les appels API Xray."""
for attempt in range(max_retries):
try:
response = func()
if response.status_code == 429:
# Rate limited — attendre et réessayer
delay = base_delay * (2 ** attempt)
retry_after = response.headers.get("Retry-After", delay)
print(f"⏳ Rate limited. Retry dans {retry_after}s...")
time.sleep(float(retry_after))
continue
response.raise_for_status()
return response
except requests.exceptions.ConnectionError:
delay = base_delay * (2 ** attempt)
print(f"🔄 Connexion perdue. Retry {attempt+1}/{max_retries} dans {delay}s...")
time.sleep(delay)
raise Exception(f"❌ Échec après {max_retries} tentatives")
# Usage
result = api_call_with_retry(
lambda: xray.post("/import/execution/junit",
data=open("report.xml", "rb").read(),
headers={**xray.headers, "Content-Type": "application/xml"})
)
Commencez par l'import JUnit dans votre CI. Puis ajoutez GraphQL pour le reporting. Puis les webhooks pour l'orchestration. Étape par étape.