Créer un script JMeter compatible PerfShop¶
Ce guide explique comment créer un scénario JMeter depuis l'interface graphique JMeter sur votre poste, puis l'uploader dans PerfShop avec le monitoring Prometheus correctement configuré.
Il intègre les leçons apprises lors de la mise au point des scénarios livrés avec PerfShop (compatibilité JMeter 5.5 + XStream, pièges XML, extraction JSON sans plugin, remontée Grafana).
Prérequis¶
- JMeter 5.5 installé localement (téléchargement)
- Plugin Prometheus Listener installé dans JMeter local (optionnel pour l'édition, obligatoire pour les tirs PerfShop)
- Accès à l'interface PerfShop JMeter UI (
http://localhost:3005ou URL publique)
Étape 1 — Structure du plan de test¶
1.1 Structure recommandée avec TransactionController¶
Pour que les métriques remontent par nom de transaction dans le dashboard Grafana,
chaque étape métier doit être enveloppée dans un TransactionController avec parent=true :
Test Plan
└── Thread Group
├── HTTP Request Defaults
├── CSV Data Set Config ← comptes utilisateurs
├── Cookie Manager ← gère JSESSIONID automatiquement
├── Header Manager ← Content-Type / Accept globaux
│
├── TransactionController [T01_Homepage] ← parent=true
│ ├── GET /api/products
│ └── GET /api/products/categories
│
├── GaussianRandomTimer ← think time entre transactions
│
├── TransactionController [T02_SearchProduct]
│ └── GET /api/products/search?q=...
│ └── JSR223PostProcessor ← extraction JSON (voir §4)
│
├── ... (autres transactions)
│
└── Prometheus Listener ← enfant du Thread Group
TransactionController parent=true — clé de la remontée Grafana
Avec parent=true, le TransactionController génère un seul sample agrégé dont
le label est le nom de la transaction (ex: T01_Homepage). C'est ce label qui
est utilisé par le plugin Prometheus pour ventiler les métriques dans Grafana.
Sans parent=true, chaque sampler HTTP interne remonte individuellement — le dashboard
devient illisible avec de nombreuses requêtes.
1.2 Nommer les transactions pour Grafana¶
Le nom du TransactionController devient le label Prometheus. Adoptez une convention :
Le dashboard Grafana filtre automatiquement les samplers techniques via :
Évitez donc ces préfixes pour vos transactions métier.
1.3 Configurer les variables de cible¶
N'écrivez jamais l'hôte et le port en dur. Utilisez les propriétés JMeter injectées par PerfShop :
Dans HTTP Request Defaults :
| Champ | Valeur à saisir |
|---|---|
| Server Name or IP | ${__P(target_host,perfshop-app)} |
| Port Number | ${__P(target_port,8080)} |
| Protocol | ${__P(target_protocol,http)} |
Étape 2 — Compatibilité JMeter 5.5 + XStream (pièges XML)¶
Trois règles critiques pour la compatibilité JMeter 5.5
Ces règles font suite à des erreurs XStream rencontrées en production.
Les ignorer provoque des NumberFormatException ou NullPointerException
au chargement du JMX.
2.1 ThreadGroup.duration doit être un stringProp¶
Problème : JMeter 5.5 utilise XStream pour désérialiser les JMX. Si ThreadGroup.duration
est déclaré en <longProp>, XStream tente de parser la valeur en long avant d'évaluer
les fonctions JMeter — ce qui provoque :
Règle : Toujours déclarer ThreadGroup.duration en <stringProp>, jamais <longProp> :
<!-- ✅ Correct -->
<stringProp name="ThreadGroup.duration">${__P(duration,300)}</stringProp>
<!-- ❌ Provoque NumberFormatException avec les variables ${__P(...)} -->
<longProp name="ThreadGroup.duration">${__P(duration,300)}</longProp>
2.2 Les stringProp dans les collectionProp doivent avoir un attribut name¶
Problème : Dans les ResponseAssertion, les codes HTTP sont stockés dans une collectionProp.
Chaque <stringProp> enfant doit avoir un attribut name correspondant au hashCode Java
de sa valeur, sinon XStream lève :
Caused by: java.lang.NullPointerException
at ConversionHelp.getUpgradePropertyName(ConversionHelp.java:253)
Règle : Utiliser le hashCode Java de la valeur comme attribut name :
<!-- ✅ Correct -->
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<!-- ❌ Provoque NullPointerException -->
<collectionProp name="Asserion.test_strings">
<stringProp>200</stringProp>
</collectionProp>
Hashcodes des codes HTTP courants (Java String.hashCode()) :
| Code HTTP | name à utiliser |
|---|---|
200 |
49586 |
201 |
49587 |
400 |
51508 |
401 |
51509 |
403 |
51511 |
404 |
51512 |
409 |
51517 |
500 |
52469 |
503 |
52472 |
Étape 3 — Extraction JSON sans plugin (JSR223PostProcessor)¶
JSONPathExtractor non disponible dans l'image PerfShop
JSONPathExtractor est fourni par le plugin bzm-json (JMeter Plugins Manager).
Ce plugin n'est pas installé dans l'image justb4/jmeter:5.5 utilisée par PerfShop.
Son utilisation provoque :
Solution : Utiliser un JSR223PostProcessor avec Groovy natif. Groovy et JsonSlurper
sont intégrés dans JMeter 5.5 sans aucun plugin.
3.1 Pattern d'extraction JSON¶
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor"
testname="Extract securityToken" enabled="true">
<stringProp name="scriptLanguage">groovy</stringProp>
<stringProp name="parameters"></stringProp>
<stringProp name="filename"></stringProp>
<stringProp name="cacheKey">true</stringProp>
<stringProp name="script">
try {
def json = new groovy.json.JsonSlurper().parseText(prev.getResponseDataAsString())
def val = json.securityToken
vars.put("securityToken", val != null ? val.toString() : "NO_TOKEN")
} catch(Exception e) {
vars.put("securityToken", "NO_TOKEN")
}
</stringProp>
</JSR223PostProcessor>
<hashTree/>
3.2 Exemples de chemins JSON courants¶
| JSONPath équivalent | Accès Groovy |
|---|---|
$.securityToken |
json.securityToken |
$.content[0].id |
json.content[0].id |
$.orderNumber |
json.orderNumber |
$.totalPages |
json.totalPages |
$.id |
json.id |
3.3 Dans JMeter GUI¶
Pour créer ce post-processor graphiquement : clic droit sur le sampler → Add → Post Processors → JSR223 Post Processor
Étape 4 — Ajouter le Prometheus Listener¶
4.1 Position dans l'arbre¶
Le PrometheusListener doit être enfant direct du Thread Group :
Et non :
4.2 Configuration dans le JMX¶
<com.github.johrstrom.listener.PrometheusListener
guiclass="com.github.johrstrom.listener.gui.PrometheusListenerGui"
testclass="com.github.johrstrom.listener.PrometheusListener"
testname="Prometheus Metrics Exporter" enabled="true">
<stringProp name="prometheus.port">${__P(prometheus.port,9270)}</stringProp>
<stringProp name="prometheus.save.threads">true</stringProp>
<stringProp name="prometheus.save.jvm">false</stringProp>
<collectionProp name="prometheus.collector_definitions">
<!-- jmeter_transactions_total (COUNTER) -->
<elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
<stringProp name="collector.metric_name">jmeter_transactions_total</stringProp>
<stringProp name="collector.type">COUNTER</stringProp>
<collectionProp name="collector.labels">
<stringProp name="102727412">label</stringProp>
<stringProp name="3059181">code</stringProp>
</collectionProp>
<stringProp name="listener.collector.listen_to">samples</stringProp>
<stringProp name="listener.collector.measuring">CountTotal</stringProp>
</elementProp>
<!-- jmeter_response_time (SUMMARY P50/P95/P99) -->
<elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
<stringProp name="collector.metric_name">jmeter_response_time</stringProp>
<stringProp name="collector.type">SUMMARY</stringProp>
<collectionProp name="collector.labels">
<stringProp name="102727412">label</stringProp>
</collectionProp>
<stringProp name="collector.quantiles_or_buckets">0.5,0.05|0.95,0.01|0.99,0.001</stringProp>
<stringProp name="listener.collector.listen_to">samples</stringProp>
<stringProp name="listener.collector.measuring">ResponseTime</stringProp>
</elementProp>
<!-- jmeter_success_ratio -->
<elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
<stringProp name="collector.metric_name">jmeter_success_ratio</stringProp>
<stringProp name="collector.type">SUCCESS_RATIO</stringProp>
<collectionProp name="collector.labels">
<stringProp name="102727412">label</stringProp>
</collectionProp>
<stringProp name="listener.collector.listen_to">samples</stringProp>
<stringProp name="listener.collector.measuring">SuccessRatio</stringProp>
</elementProp>
</collectionProp>
</com.github.johrstrom.listener.PrometheusListener>
<hashTree/>
Étape 5 — CSV Data Set et variables¶
Pour les scénarios multi-utilisateurs, utilisez un CSVDataSet avec shareMode.thread
(un compte différent par thread) :
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet"
testname="Comptes utilisateurs" enabled="true">
<stringProp name="filename">/scripts/users.csv</stringProp>
<stringProp name="fileEncoding">UTF-8</stringProp>
<stringProp name="variableNames">userEmail,userPassword,userName</stringProp>
<stringProp name="delimiter">,</stringProp>
<boolProp name="ignoreFirstLine">true</boolProp>
<boolProp name="recycle">true</boolProp>
<boolProp name="stopThread">false</boolProp>
<stringProp name="shareMode">shareMode.thread</stringProp>
</CSVDataSet>
<hashTree/>
Le fichier users.csv est monté dans le container sur /scripts/users.csv.
Étape 6 — Uploader et lancer dans PerfShop¶
- Sauvegarder le
.jmx: File → Save Test Plan As - Ouvrir PerfShop JMeter UI → 🗂️ Gérer
- Choisir le dossier cible → 📤 → sélectionner le
.jmx - Lancer avec
▶ Lancer le tir
Les métriques apparaissent dans le dashboard Grafana perfshop-jmeter-live au bout de ~10 secondes,
ventilées par nom de TransactionController.
Récapitulatif — Checklist avant upload¶
| Point de contrôle | ✅ Attendu |
|---|---|
target_host |
${__P(target_host,perfshop-app)} |
target_port |
${__P(target_port,8080)} — jamais 9080 |
ThreadGroup.duration |
<stringProp> — jamais <longProp> avec ${__P(...)} |
stringProp dans collectionProp |
Attribut name avec hashCode Java de la valeur |
| Extraction JSON | JSR223PostProcessor Groovy — jamais JSONPathExtractor (plugin absent) |
TransactionController |
parent=true sur chaque étape métier |
| PrometheusListener | Enfant du Thread Group, pas du Test Plan |
| Port Prometheus | ${__P(prometheus.port,9270)} |
| Think time | GaussianRandomTimer entre chaque transaction |
| Pas d'URL en dur | Ni localhost, ni IP fixe, ni hostname public |
Étape 7 — Guide pas-à-pas via JMeter GUI¶
Ce guide montre comment créer un scénario parcours métier complet depuis l'interface graphique JMeter, sans éditer le XML à la main.
7.1 Créer le plan de test¶
- Lancer JMeter :
jmeter.bat(Windows) oujmeter(Linux/Mac) - File → New pour un plan vierge
- Cliquer sur Test Plan → renommer :
PerfShop — Mon scénario - Dans le panneau du bas, cocher Functional Test Mode : Non
7.2 Ajouter un Thread Group¶
Clic droit sur Test Plan → Add → Threads (Users) → Thread Group
| Paramètre | Valeur recommandée |
|---|---|
| Number of Threads | ${__P(users,5)} |
| Ramp-up period | ${__P(ramp_time,30)} |
| Duration | ${__P(duration,300)} |
| Loop Count | -1 (infini avec scheduler) |
| Scheduler | ✅ Coché |
| On Sample Error | Continue |
Ne jamais mettre un nombre fixe pour users/duration
Utilisez toujours ${__P(users,5)} — cela permet à l'interface JMeter UI de PerfShop
de piloter le nombre de vUsers et la durée sans modifier le JMX.
7.3 Ajouter les éléments de configuration¶
Clic droit sur Thread Group → Add :
HTTP Request Defaults (Config Element → HTTP Request Defaults)
| Champ | Valeur |
|---|---|
| Server Name | ${__P(target_host,perfshop-app)} |
| Port | ${__P(target_port,8080)} |
| Protocol | ${__P(target_protocol,http)} |
| Encoding | UTF-8 |
HTTP Header Manager (Config Element → HTTP Header Manager)
Ajouter deux headers :
| Name | Value |
|---|---|
Content-Type |
application/json |
Accept |
application/json |
HTTP Cookie Manager (Config Element → HTTP Cookie Manager)
- Clear cookies each iteration : ✅
CSV Data Set Config (Config Element → CSV Data Set Config)
| Champ | Valeur |
|---|---|
| Filename | /scripts/users.csv |
| Variable Names | userEmail,userPassword,userName |
| Delimiter | , |
| Ignore first line | ✅ |
| Recycle on EOF | ✅ |
| Share Mode | shareMode.all |
shareMode.all — évite les collisions de comptes
Avec shareMode.thread, chaque thread progresse indépendamment et plusieurs threads
peuvent tomber sur le même compte simultanément. Avec shareMode.all, tous les threads
partagent un curseur global qui avance séquentiellement — chaque thread reçoit un compte
différent à chaque itération. Indispensable pour éviter l'erreur "Commande en double
détectée" du backend PerfShop (cooldown anti-doublon de 3 secondes par user+panier).
7.4 Ajouter une transaction¶
Pour chaque étape métier (T01, T02...):
- Clic droit sur Thread Group → Add → Logic Controller → Transaction Controller
- Nommer :
T01_Homepage - Cocher Generate parent sample : ✅ (c'est le
parent=trueindispensable pour Grafana) - Clic droit sur la transaction → Add → Sampler → HTTP Request
- Configurer la requête : méthode, path, body
7.5 Ajouter un think time (GaussianRandomTimer)¶
Clic droit sur Thread Group → Add → Timer → Gaussian Random Timer
| Champ | Valeur |
|---|---|
| Constant Delay | 2000 (ms) |
| Deviation | 600 (ms) |
Placer ce timer après chaque TransactionController (au niveau du Thread Group, pas à l'intérieur).
7.6 Ajouter une extraction JSON (JSR223PostProcessor)¶
Clic droit sur le sampler HTTP → Add → Post Processors → JSR223 Post Processor
- Script Language :
groovy - Cache compiled script if possible : ✅
- Script :
try {
def json = new groovy.json.JsonSlurper().parseText(prev.getResponseDataAsString())
def val = json.securityToken
vars.put("securityToken", val != null ? val.toString() : "NO_TOKEN")
} catch(Exception e) {
vars.put("securityToken", "NO_TOKEN")
}
7.7 Ajouter une assertion de réponse¶
Clic droit sur le sampler HTTP → Add → Assertions → Response Assertion
- Field to Test :
Response Code - Pattern Matching Rules :
Equals - Patterns to test :
200
Pour vérifier 200 ou 201 (OR) :
- Patterns to test : ajouter 200 et 201
- Pattern Matching Rules : OR (cocher "OR")
7.8 Ajouter une assertion Groovy (JSR223Assertion)¶
Pour des vérifications complexes (ex: canCheckout = true) :
Clic droit sur le sampler → Add → Assertions → JSR223 Assertion
try {
def json = new groovy.json.JsonSlurper().parseText(prev.getResponseDataAsString())
if (json.canCheckout != true) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("canCheckout = false")
}
} catch(Exception e) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("JSON invalide : " + e.getMessage())
}
Jamais JSONPathAssertion dans PerfShop
Même règle que JSONPathExtractor : le plugin bzm-json est absent.
Remplacer systématiquement par JSR223Assertion Groovy.
7.9 Ajouter un JSR223PreProcessor (avant un sampler)¶
Pour préparer des variables dynamiques avant l'exécution d'un sampler :
Clic droit sur le sampler → Add → Pre Processors → JSR223 Pre Processor
Exemple — sélectionner un mode de livraison au hasard sans __chooseRandom :
def methods = ["standard", "standard", "express", "premium"]
def r = new Random()
vars.put("shippingMethodToUse", methods[r.nextInt(methods.size())])
PreProcessor enfant du sampler : s'exécute AVANT le sampler
Un JSR223PreProcessor positionné comme enfant d'un HTTPSamplerProxy
s'exécute avant ce sampler. Les variables qu'il définit sont disponibles
dans le body de ce sampler.
7.10 Ajouter le Prometheus Listener¶
Clic droit sur Thread Group → Add → Listener → jp@gc - Prometheus Listener
- Port :
${__P(prometheus.port,9270)} - Save threads : ✅
- Save JVM : ☐
Ajouter trois métriques (bouton +) :
| Metric Name | Type | Labels | Measuring |
|---|---|---|---|
jmeter_transactions_total |
COUNTER | label, code |
CountTotal |
jmeter_response_time |
SUMMARY | label |
ResponseTime |
jmeter_success_ratio |
SUCCESS_RATIO | label |
SuccessRatio |
Pour jmeter_response_time, ajouter les quantiles : 0.5,0.05|0.95,0.01|0.99,0.001
7.11 Sauvegarder et tester localement¶
- File → Save Test Plan As → nommer
mon-scenario.jmx - Lancer localement : Run → Start (ou
Ctrl+R) - Vérifier dans View Results Tree que les assertions passent
- Uploader dans PerfShop JMeter UI → 🗂️ Gérer → 📤
Étape 8 — Pièges supplémentaires découverts sur PerfShop¶
Ces pièges sont issus de la mise au point du scénario parcours-metier-nominal.jmx.
❌ Piège 6 — Scripts Groovy sur une seule ligne avec \n littéraux¶
Symptôme :
startup failed:
Script166.groovy: 1: Unexpected character: '\' @ line 1, column 6.
try {\n def json = new groovy.json.JsonSlurper()...
Cause : Quand un JMX est édité manuellement et que le script Groovy est mis sur une seule ligne
avec des \n littéraux (backslash + n), JMeter 5.5 / Groovy 3.0 ne les interprète pas comme des
sauts de ligne. Le Groovy est alors syntaxiquement invalide.
<!-- ❌ MAUVAIS — \n littéraux, script sur une ligne -->
<stringProp name="script">try {\n def json = ...\n} catch(Exception e) {\n}</stringProp>
<!-- ✅ CORRECT — vrais sauts de ligne dans le XML -->
<stringProp name="script">
try {
def json = new groovy.json.JsonSlurper().parseText(prev.getResponseDataAsString())
vars.put("myVar", json.myField?.toString() ?: "default")
} catch(Exception e) {
vars.put("myVar", "default")
}
</stringProp>
Règle : Les scripts Groovy dans les stringProp doivent toujours contenir de vrais sauts de ligne XML.
❌ Piège 7 — __chooseRandom avec virgule finale génère des valeurs vides¶
Symptôme : Requêtes avec 400/422, body JSON avec des champs vides ("street":"").
Cause : __chooseRandom(val1,val2,val3,) — la virgule finale est interprétée comme un item supplémentaire vide.
Quand JMeter tire le dernier item, la variable vaut "" et le backend rejette le JSON.
# ❌ MAUVAIS — virgule finale
${__chooseRandom(standard,express,premium,)}
# ✅ CORRECT — sans virgule finale (mais déconseillé, voir ci-dessous)
${__chooseRandom(standard,express,premium)}
# ✅ ENCORE MIEUX — JSR223PreProcessor avec tableau Groovy
def methods = ["standard", "express", "premium"]
vars.put("method", methods[new Random().nextInt(methods.size())])
Règle : Ne jamais utiliser __chooseRandom dans PerfShop — toujours utiliser un JSR223PreProcessor avec un tableau Groovy.
❌ Piège 8 — && dans les fonctions JMeter inline cause XmlPullParserException¶
Symptôme :
Caused by: org.xmlpull.v1.XmlPullParserException: entity reference names can not start with character '&'
Cause : Le caractère & dans un attribut XML doit être échappé en &. Dans un stringProp,
&& (AND logique Groovy) est du XML invalide. Le parser XStream (XmlPullParser) le rejette.
<!-- ❌ MAUVAIS — && invalide dans stringProp XML -->
<stringProp name="Argument.value">{"id":${__groovy(vars.get('a') != null && !vars.get('a').equals('0') ? vars.get('a') : '1',)}}</stringProp>
<!-- ✅ CORRECT — JSR223PreProcessor séparé AVANT le sampler -->
// Dans un JSR223PreProcessor enfant du sampler :
def a = vars.get("a")
vars.put("idToUse", (a != null && !a.equals("0")) ? a : "1")
Règle : Toute logique conditionnelle doit être dans un JSR223PreProcessor séparé.
Ne jamais mettre de &&, ||, ou <> dans une stringProp sans échappement XML.
❌ Piège 9 — Protection anti-doublon du backend (HTTP 500)¶
Symptôme : POST /api/orders retourne 500 avec "Commande en double détectée" en charge.
Cause : Le backend PerfShop implémente une protection anti-double-clic (anomalie A5).
En mode nominal (chaos level 0), si le même utilisateur crée deux commandes avec le même
contenu de panier dans une fenêtre de 3 secondes, la deuxième est rejetée avec une
RuntimeException → Spring retourne HTTP 500.
En charge avec plusieurs threads partageant le même compte (par ex. user1@perfshop.com),
cette fenêtre est facilement dépassée.
Solution JMX : Utiliser shareMode.all dans le CSV Data Set pour que les threads
se partagent les comptes de façon séquentielle et n'aient jamais le même utilisateur actif.
Solution assertion : Accepter le 500 doublon comme un succès conditionnel via JSR223Assertion :
def code = prev.getResponseCode()
if (code == "500") {
def body = prev.getResponseDataAsString()
if (body.contains("doublon") || body.contains("duplicate")) {
// Doublon anti-double-clic — comportement normal en charge
// Ne pas faire échouer la transaction
return
}
}
// Vérifier le succès normal
def json = new groovy.json.JsonSlurper().parseText(prev.getResponseDataAsString())
if (json.success != true) {
AssertionResult.setFailure(true)
AssertionResult.setFailureMessage("success=false : " + prev.getResponseDataAsString().take(200))
}
❌ Piège 10 — orderId vide dans l'URL cause URISyntaxException¶
Symptôme :
Cause : Si T08 (création commande) échoue, ${orderId} reste vide. L'URL
/api/orders//details est syntaxiquement invalide pour java.net.URI.
Solution : Ajouter un JSR223PreProcessor avant le sampler GET /api/orders/${orderIdToFetch}/details
qui met un fallback "0" si orderId est absent :
def oid = vars.get("orderId")
if (oid == null || oid.equals("") || oid.equals("0")) {
vars.put("orderIdToFetch", "0")
} else {
vars.put("orderIdToFetch", oid)
}
Utiliser ${orderIdToFetch} dans le path au lieu de ${orderId}.
Récapitulatif complet — Règles définitives JMX PerfShop¶
| Règle | Description |
|---|---|
✅ shareMode.all |
CSV Data Set — distribution séquentielle des comptes entre threads |
| ✅ Scripts Groovy multiligne | Vrais sauts de ligne dans les stringProp script |
✅ JSR223PreProcessor pour la logique |
Jamais de &&, ||, __groovy() inline dans stringProp |
✅ JSR223PostProcessor pour l'extraction |
Jamais JSONPathExtractor (plugin absent) |
✅ JSR223Assertion pour les assertions JSON |
Jamais JSONPathAssertion (plugin absent) |
✅ JSR223PreProcessor pour les données aléatoires |
Jamais __chooseRandom avec virgule finale |
| ✅ Fallback sur les variables critiques | Vérifier orderId, foundProductId, foundProductPrice avant utilisation dans une URL |
✅ TransactionController parent=true |
Indispensable pour la remontée Grafana |
✅ ${__P(...)} pour tous les paramètres pilotables |
Jamais de valeur en dur pour users, duration, target_host |
✅ <stringProp> pour ThreadGroup.duration |
Jamais <longProp> avec ${__P(...)} |
✅ Attribut name hashCode sur les stringProp dans collectionProp |
Évite le NPE XStream |
| ✅ PrometheusListener enfant du ThreadGroup | Pas du TestPlan |