• 10 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 28/11/2023

Cloisonnez vos tests

Ne vous mockez pas

Nous avons jusqu’à maintenant mis une API à disposition et écrit des tests sur celle-ci. Mais il existe un cas compliqué à tester, lorsque nous sommes consommateurs d’une API.

Lors de l’exécution des tests, nous ne souhaitons pas que les appels soient réalisés. Cela pourrait créer des données incohérentes sur le système cible, ou empêcher simplement certains tests si, par exemple, la création de doublons de noms n’est pas permise (comme nous l’avons mis en place dans la partie précédente !). Et imaginez que l’API tierce soit payante, alors chaque exécution d’un test serait alors facturée.

Pour pallier cela, il existe ce qu’on appelle des mocks.

Un mock se base sur le principe du Monkey Patching, c'est-à-dire modifier une section de code pour une durée limitée, sans toucher à sa version originale. Dans le cas d’un mock, cela veut dire que lors de l'exécution du test qui réalise l’appel, nous allons Monkey Patcher l’appel réseau, et simuler une réponse qui correspond à notre cas de test.

Comme un exemple vaut mille mots, voyons cela tout de suite !

Appelez une API externe

Donnons aux consommateurs encore plus de détails sur nos produits grâce à Open Food Facts, une base de données sur les produits alimentaires. Nous allons extraire l’écoscore de cet appel et le retourner dans notre endpoint de détail d’un produit.

Mettons en place un appel à l’API d’Open Food Facts sur un produit spécifique. L’appel sera réalisé sur l’URL https://world.openfoodfacts.org/api/v0/product/3229820787015.json.

Le client fait un appel à l'API, qui envoie aussi un appel vers Open Food Facts. L'API reçoit la réponse du site et transmet le détail du produit au client.
L'API nous permet d'incorporer l'écoscore d'Open Food Facts dans le détail d'un produit

Réalisons cet appel et retournons les données dans un nouvel attribut nommé ecoscore. Nous verrons ensuite comment résoudre la problématique de l’appel tiers dans nos tests.

Commençons par ajouter requests  à nos dépendances pour réaliser l’appel.

Django==3.2.5

djangorestframework==3.12.4

requests==2.26.0

Puis installons requests  avec  pip install -r requirements.txt  avant de mettre en place une propriété sur notre model Category  qui permet :

  • De faire l’appel à Open Food Facts ;

  • Puis de renvoyer l’écoscore ainsi qu’une méthode qui va réaliser l’appel.

def call_external_api(self, method, url):
    # l'appel doit être le plus petit possible car appliquer un mock va réduire la couverture de tests
    # C'est cette méthode qui va être monkey patchée
    return requests.request(method, url)
 
@property
def ecoscore(self):
    # Nous réalisons l'appel à open food fact
    response = self.call_external_api('GET', 'https://world.openfoodfacts.org/api/v0/product/3229820787015.json')
    if response.status_code == 200:
    # et ne renvoyons l'écoscore que si la réponse est valide
        return response.json()['product']['ecoscore_grade']

Modifions notre serializer de liste de produits pour qu’il retourne l’écoscore.

class ProductListSerializer(serializers.ModelSerializer):
 
    class Meta:
        model = Product
        fields = ['id', 'date_created', 'date_updated', 'name', 'category', 'ecoscore']

Nous pouvons maintenant voir que l’écoscore est correctement retourné par notre API depuis les données d’Open Food Facts.

Le champ écoscore a été bien ajouté aux données retournées pour chaque produit dans notre Product Viewset List
Les écoscores sont ajoutés dans les détails de nos produits

Lancez vos tests sans appel externe

Nos tests sont maintenant en erreur. Nous pourrions les corriger en mettant explicitement la valeur “d” dans l’écoscore... Mais si vous coupez votre connexion Internet alors le test va retomber en erreur, car l’appel ne peut pas être réalisé.

Nous allons donc mocker cet appel pour qu’il puisse être réalisé dans toutes les conditions.

Commençons par créer un fichier mocks.py  dans l’application Django shop. Celui-ci contiendra toutes les données permettant de mocker notre appel, que nous utiliserons ensuite dans nos tests :

import requests
 
# l'ecoscore est stocké dans une constante et sera réutilisé dans nos tests
ECOSCORE_GRADE = 'd'
 
def mock_openfoodfact_success(self, method, url):
    # Notre mock doit avoir la même signature que la méthode à mocker
    # À savoir les paramètres d'entrée et le type de sortie
    def monkey_json():
    # Nous créons une méthode qui servira à monkey patcher response.json()
        return {
            'product': {
            'ecoscore_grade': ECOSCORE_GRADE
            }
        }
 
    # Créons la réponse et modifions ses valeurs pour que le status code et les données
    # correspondent à nos attendus
    response = requests.Response()
    response.status_code = 200
    # Nous monkey patchons response.json
    # Attention à ne pas mettre les (), nous n'appelons pas la méthode mais la remplaçons
    response.json = monkey_json
    return response

Et adaptons nos tests en utilisant le décorateur mock.patch.

# Nous aurons besoin de mock pour appliquer notre mock
from unittest import mock
from django.urls import reverse_lazy, reverse
from rest_framework.test import APITestCase
 
from shop.models import Category, Product
# importons notre mock et la valeur attendue de l'ecoscore
from shop.mocks import mock_openfoodfact_succes, ECOSCORE_GRADE
 
class ShopAPITestCase(APITestCase):
 
# ....
 
    def get_product_detail_data(self, product):
    # Modifions les données attendues pour le détail d'un produit en ajoutant l'ecoscore
        return {
            'id': product.pk,
            'name': product.name,
            'date_created': self.format_datetime(product.date_created),
            'date_updated': self.format_datetime(product.date_updated),
            'category': product.category_id,
            'articles': self.get_article_detail_data(product.articles.filter(active=True)),
            'ecoscore': ECOSCORE_GRADE  # la valeur de l'ecoscore provient de notre constante utilisée dans notre mock
        }
 
class TestProduct(ShopAPITestCase):
 
    @mock.patch('shop.models.Product.call_external_api', mock_openfoodfact_success)
    # Le premier paramètre est la méthode à mocker
    # Le second est le mock à appliquer
    def test_detail(self):
        response = self.client.get(reverse('product-detail', kwargs={'pk': self.product.pk}))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(self.get_product_detail_data(self.product), response.json())

Nos tests s'exécutent à présent en succès, même sans aucune connexion Internet !

Voyons la mise en place d'un mock de plus près, dans le screencast ci-dessous :

En résumé

  • Une API peut très bien faire des appels à d’autres API.

  • Lors d’un appel à une API externe, il faut prévoir que cette API puisse ne pas répondre, afin d’éviter que notre API ne fonctionne plus.

  • Lors d’un appel à une API externe, il faut mettre en place un mock pour pouvoir tester dans tous les cas d’usage, même sans connexion Internet.

Dans cette partie, nous avons rendu nos endpoints plus performants et avons mis notre API à l’épreuve de tests grâce aux mocks. Avant de sécuriser notre API avec l’authentification, validez vos acquis de cette partie dans le quiz ! Je vous attends dans la partie 3 !

Exemple de certificat de réussite
Exemple de certificat de réussite