Logo Spiria

“Resto”, un module en Python pour tester le back-end web d’une API RESTful

29 octobre 2020.

Je crois fermement que si quelque chose n’est pas testé, il n’est alors probablement pas fonctionnel. D’autre part, écrire des tests peut être fastidieux. Tout outil pour aider à accélérer et à simplifier les tests est ainsi bienvenu. Dans un récent projet, j'ai dû écrire le back-end d'un site web, en fournissant une API RESTful. J'ai décidé d'écrire le back-end en Python car j'avais eu une bonne expérience en l'utilisant dans un site web. J’ai cherché des outils et des bonnes pratiques pour tester une API RESTful, mais je n’ai pas obtenu de suggestions satisfaisantes. J’ai donc décidé d’écrire mon propre module pour aider l’écriture de tests. Je l’ai nommé resto.

L’un des défis dans le design de tout outil est de le rendre utile tout en en restant aussi simple que possible. Les outils trop complexes restent inutilisés. Selon moi, il vaut mieux être imparfait, mais simple ; et resto suit cette philosophie. Je l’avais décrit en quelques mots à mes comme étant “JSON in, JSON out”. (Il est devenu aussi “headers in, headers out.”)

Donnez à resto les données d’entrée à votre API REST, le résultat attendu et il déterminera si cela a fonctionné ou vous fournira un delta minimal de la différence en cas d’échec.

Exemple

Avant d’entrer dans les détails de son design, voici un exemple de l’utilisation de resto dans un test d’intégration. Dans cet exemple, nous appelons une API REST /dogs avec une méthode POST pour créer un nouveau chien.

Une fois les données fournies, l’appel à la fonction call() fait tout le travail d’envoi de la requête et de la comparaison des résultats. Nous recevons simplement un dictionnaire de toutes les différences, que l’on s’attend à être vides lorsque le test fonctionne.

Python
def test_dogs_create_successful():
    """
    The goal of the test is to verify that the /dogs end-point
    can create a new dog when POST and return the correct info.
    """
    cfg = resto.Config()

    in_headers = prepare_login_for_write_tests(cfg)

    in_dog = {
        "first_name": "Wabby",
        "last_name": "Husky",
    }

    out_dog = {
        "dog": {
            "id": 4,
            "first_name": "Wabby",
            "last_name": "Husky",
        }
    }

    exp = resto.Expected(
        url = f’/dogs’,
        method = Method.POST,
        in_json = in_dog,
        in_headers = in_headers,
        out_json = out_dog,
        out_headers = { ‘Location’: f’{cfg.base_url}/dogs/4’ },
        status_code = 201
    )
    diff = exp.call(cfg)
    self.assertEqual({}, diff)

L’API resto

L’API resto contient deux classes et une énumération. La classe “Config” contient la configuration pour resto, qui pour l’instant ne contient que l’URL de base de l’API à tester. L’énumération de la “Method” permet de sélectionner la méthode HTTP à utiliser : GET, POST, PUT ou DELETE. Le cœur du module Resto se trouve dans la classe “Expected”. Elle fait l’appel à un point d’entrée web REST et compare les résultats reçus.

La classe “Expected” reçoit la description de l’API REST à appeler et les résultats attendus. Aucun de ces éléments décrivant l’API REST n’est requis. Resto profite des paramètres nommés (named parameters) de Python pour les rendre tous facultatifs. En détail, voici la description de l’API REST et les résultats attendus :

  • url : l’URL qui sera appelée.
  • method : la méthode HTTP à utiliser.
  • params : les paramètres de l’URL.
  • in_headers : les en-têtes (headers) HTTP.
  • out_headers : les en-têtes HTTP attendus en sortie.
  • in_json : le JSON à envoyer.
  • out_json : le JSON attendu en sortie.
  • out_json_strict : choix de comparer les JSON exactement ou non.
  • status_code : le code de statut attendu.

L’option de comparaison stricte JSON contrôle le degré de rigueur de la comparaison. Lorsque “True” (la valeur par défaut), la sortie JSON doit correspondre exactement à ce qui était attendu. Sinon, les champs supplémentaires dans le JSON reçu sont ignorés. Ceci est utile si vous voulez seulement tester un sous-ensemble du résultat.

Toutes les descriptions d’entrées/sorties sont sous forme de dict Python. Cela simplifie la déclaration du JSON et des en-têtes. La comparaison entre le JSON et les en-têtes suit les règles naturelles de comparaison en Python, mais avec certaines capacités supplémentaires pour aider à comparer les textes.

Si le texte attendu commence par un tilde, “~”, le texte restant doit seulement se trouver dans le texte reçu. Par exemple, si le texte attendu est “~autorisation”, il correspondrait à tout texte contenant “autorisation”.

Si le texte est une étoile, “*”, alors il correspond à n’importe quel texte. Ceci est utile lorsque nous voulons vérifier que nous recevons bien une valeur JSON, mais que son contenu est inconnu. Par exemple, pour les jetons d’autorisation (auth token), le texte du jeton généré peut ne pas être connu. En utilisant “*” comme texte attendu, on lui permet de correspondre à n’importe quel jeton d’autorisation.

Si le design peut sembler simple, cela va un peu plus loin en essayant de correspondre le mieux possible aux JSON ou aux en-têtes attendus. Ceci résout le problème suivant : s’il manque un élément à un tableau, resto essaiera de faire correspondre au mieux tous les autres éléments afin de générer le delta de différence minimum.

Une fois que la description de l’appel REST “Expected” est créée, l’appel à l’API REST se fait via la fonction call(). Cette fonction ne prend qu’un seul paramètre : la configuration, de sorte que l’URL de base peut être utilisée pour contacter l’API REST. De son côté, la différence entre le résultat attendu et le résultat réel est retournée sous forme de dict Python par la fonction call(). Ainsi, vérifier le succès d’une opération est aussi simple que de comparer avec un dict vide. Pour afficher la différence, il suffit d’afficher ce dictionnaire.

Disponibilité

Nous avons utilisé ce module pour écrire des centaines de tests d’intégration pour un projet interne. La simplicité de l’interface rend l’écriture de tests un jeu d’enfant et constitue un facteur important dans notre capacité à tester minutieusement. Avec la combinaison de tests d’intégration et de tests unitaires, nous avons obtenu une couverture de code de presque 100 % pour presque toutes les fonctionnalités du projet.

Ce module resto est disponible dans ce repo GitHub. Le code source est dans le fichier resto.py.

J’espère que vous trouverez ce module aussi utile qu’il l’a été pour moi.