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:

karate-report1
karate-report2

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.