Tests automatisés d’api avec Karate
Dans le cadre de nos projets, nous sommes régulièrement amenés à définir et à développer des API et des services REST.
Nous avons à plusieurs reprises fait le choix d’utiliser Karate pour réaliser des scénarios de tests automatisés sur ce type de projet.
Karate est idéal pour mettre au point rapidement une série de tests représentant des enchaînements d’appels de services REST, de plus il s’intègre parfaitement avec l’ outil d’intégration continue Jenkins.
Nous allons voir comment écrire et organiser des scénarios de tests avec Karate, ainsi que son intégration à Jenkins.
Posons le contexte
Pour illustrer la réalisation de tests automatisés avec Karate, prenons l’exemple d’une api permettant à un client d’acheter des produits et de payer via une de ses cartes de paiements.
Notre api permet donc à nos clients:
1 – de créer un panier
POST /carts
2 – d’ajouter des produits dans le panier
POST /carts/{{cartId}}/items
{
"itemId": '#(itemId)'
}
3 – de modifier la quantité des produits du panier
PUT /carts/{{id}}/items/{{itemId}}
{
"quantity": '#(quantity)'
}
4 – de consulter son panier
GET /carts/{{cartId}}
5 – de récupérer ses moyens de paiements
GET /carts/{{cartId}}/wallet
6 – de payer le panier
POST /carts/{{cartId}}/payment?cardId={{cardId}}
Réalisons les scénarios réutilisables
Pensons réutilisabilité et commençons par créer 6 fichiers feature contenant chacun un scénario correspondant à une requête vers notre api.
Pour chaque requête, nous enregistrons le code de retour http et la réponse afin de pouvoir y accéder ultérieurement.
1 – Créer un panier
Fichier: createCart.feature
Feature: API - createCart
Scenario: create a cart
When url BASE_URL + '/carts'
And request {}
And method post
* def statusCode = responseStatus
* def body = $
Ce scénario effectue une requête http POST sur /carts
2 – Ajouter des produits dans le panier
Fichier: addItemToCart.feature
Feature: API - addItemToCart
Scenario: add an item to a cart
When url BASE_URL + '/carts/' + cartId + '/items'
And request
"""
{
"itemId": '#(itemId)'
}
"""
And method post
* def statusCode = responseStatus
* def body = $
Ce scénario effectue une requête http POST sur /carts/{{cartId}}/items
Il a deux paramètres:
- cartId: identifiant du panier
- itemId: identifiant du produit à ajouter au panier
3 – Modifier la quantité des produits du panier
Fichier: updateCartQuantity.feature
Feature: API - updateCartQuantity
Scenario: update cart item quantity
When url BASE_URL + '/carts/' + cartId + '/items/' + itemId
And request
"""
{
"quantity": '#(quantity)'
}
"""
And method put
* def statusCode = responseStatus
* def body = $
Ce scénario effectue une requête http PUT sur /carts/{{id}}/items/{{itemId}}
Il a trois paramètres:
- cartId: identifiant du panier
- itemId: identifiant du produit à mettre à jour
- quantity: nouvelle quantité pour le produit à mettre à jour
4 – Consulter son panier
Fichier: retrieveCart.feature
Feature: API - retrieveCart
Scenario: retrieve a cart
When url BASE_URL + '/carts/' + cartId
And request {}
And method get
* def statusCode = responseStatus
* def body = $
Ce scénario effectue une requête http GET sur /carts/{{cartId}}
Il a un paramètre
- cartId: identifiant du panier
5 – Récupérer ses moyens de paiements
Fichier: getWallet.feature
Feature: API - getWallet
Scenario: get wallet
When url BASE_URL + '/carts/' + cartId + '/wallet'
And request {}
And method get
* def statusCode = responseStatus
* def body = $
Ce scénario effectue une requête http GET sur la ressource /carts/{{cartId}}/wallet
Il a un paramètre
- cartId: identifiant du panier
6 – Payer le panier
Fichier: postPayment.feature
Feature: API - postPayment
Scenario: Pay a cart
When url BASE_URL + '/carts/' + cartId + '/payment'
And param cardId = cardId
And request {}
And method post
* def statusCode = responseStatus
* def body = $
Ce scénario effectue une requête http POST sur /carts/{{cartId}}/payment
Il a deux paramètres
- cartId: identifiant du panier
- cardId: identifiant de la carte de paiement
Réalisons les scénarios de tests
Commençons par créer un nouveau fichier « feature » avec un block « Background » permettant de rejouer des instructions avant chaque scénario. Ici les instructions du block « Background » correspondent à la création d’un panier. Nous utilisons l’instruction call pour réutiliser les scénarios précédemment créés. Afin que l’identifiant du panier soit visible dans tous les scénarios, nous l’enregistons dans une variable cartId.
Feature: API
Background:
When def createCartResult = call read(createCart)
Then match createCartResult.statusCode == 200
* def cartId = createCartResult.body.id
Ajoutons un scénario qui vérifie simplement la bonne création d’un panier. La création du panier est effectuée dans le block Background, il est donc inutile de répéter cette opération dans le scénario.
Scenario: Create a cart
Then match createCartResult.body.status == 'IN_PROGRESS'
And match createCartResult.body.items == []
Ajoutons un scénario qui teste la consultation d’un panier. Un nouveau panier est créé avant l’exécution du scénario grâce aux instructions du block Background
Scenario: Retrieve a cart
When def r = call read(retrieveCart)
Then match r.statusCode == 200
And match r.body.status == 'IN_PROGRESS'
Ajoutons un scénario qui vérifie l’ajout d’un article dans un panier.
Scenario: add an item
When def r = call read(addItemToCart) { itemId: 'XXX' }
Then match r.statusCode == 200
And match r.body.status == 'IN_PROGRESS'
And match r.body.items[0].itemId == 'XXX'
Ajoutons un scénario qui ajoute un article au panier et vérifie la mise à jour de la quantité
Scenario: add an item and update its quantity
When def addItemToCartResult = call read(addItemToCart) { itemId: 'XXX' }
Then match addItemToCartResult.statusCode == 200
When def r = call read(updateCartQuantity) { itemId: '#(addItemToCartResult.body.items[0].itemId)', quantity: 3 }
Then match r.statusCode == 200
And match r.body.status == 'IN_PROGRESS'
And match r.body.items[0].quantity == 3
Ajoutons un dernier scénario qui ajoute un produit dans un panier, récupère une carte de paiement, utilise cette carte pour payer, vérifie l’état du panier et la création d’un ticket.
Scenario: Pay a cart with a card
# add an item to cart
When def addItemToCartResult = call read(addItemToCart) { itemId: 'XXX' }
Then match addItemToCartResult.statusCode == 200
# retrieve a card
When def getWalletResult = call read(getWallet)
Then match getWalletResult.statusCode == 200
* def cardId = getWalletResult.items[0].id
# pay
When def r = call read(postPayment) { cardId: '#(cardId)' }
Then match r.statusCode == 200
And match r.body.status == 'PAYMENT'
And match r.body.payment ==
"""
{
"id": '#string',
"creationDate": '#string',
"status": "SUCCESS"
"""
# check status FINISHED and ticket is available
When def r = call read(retrieveCart)
Then match r.statusCode == 200
And match r.body.status == 'FINISHED'
And match r.body.ticket == { ticketNumber: '#string', creationDate: '#string' }
Intégration avec Jenkins
Le plugin Cucumber Reports permet d’intégrer facilement nos scénarios Karate à Jenkins.
En utilisant un jenkins pipeline, il suffit d’ajouter dans son Jenkinsfile après son build:
cucumber fileIncludePattern: '**/target/surefire-reports/*.json'
Jenkins construira un reporting comme illustré ci-dessous:
Conclusion
Karate est un outil très efficace pour tester des api et des services rest.
Sa faculté à s’interfacer avec du code java et javascript est très utile pour ajouter des scripts et interagir avec d’autres outils comme Selenium Webdriver.
Enfin Karate reposant sur Cucumber, nous pouvons profiter des plugins Cucumber pour une intégration avec Jenkins.