tominardi.fr

La qualité dans un projet web, Partie 6 - Fixtures et mocks

27 mars 2023

Nouvel article concernant la qualité logiciel, aujourd'hui, nous allons mettre l'accent sur les données de test.

Le dossier

Rappel du contexte

Cet article fait partie d'une série d'articles visant à énumérer les différents éléments de qualité à mettre en place au démarrage d'un projet.

Cet article sera assez court, c'est surtout un complément à la partie 5. Comme d'habitude, on ne rentrera pas dans les détails, le message étant plutôt : Pense à mettre en place tes solutions avant de commencer à coder.

Fixtures

Lorsqu'on va tester, on va avoir besoin de se mettre dans un état de départ, avec un jeu de données en particulier par exemple.

Les fixtures sont des fonctions ou des méthodes qui permettent de préparer l'environnement de test avant l'exécution de chaque test. C'est ce qu'on va utiliser pour initialiser des objets, des bases de données, des fichiers temporaires, etc.

Dans Django, les fixtures sont des fichiers de données, en JSON ou en YAML, que l'on va pouvoir charger avant d'effectuer les tests. On pourra charger des jeux de données spécifiques pour certains tests.

Par exemple, voici un fichier de fixture :

[
  {
    "model": "myapp.musician",
    "pk": 1,
    "fields": {
      "name": "Joe Strummer",
      "band": "The Clash",
      "instrument": "Vocals, Guitar",
      "years_active": "1976-1986, 2001-2002"
    }
  },
  {
    "model": "myapp.musician",
    "pk": 2,
    "fields": {
      "name": "Dee Dee Ramone",
      "band": "The Ramones",
      "instrument": "Bass",
      "years_active": "1974-1989"
    }
  }
]

Ces données pourront être chargées dans la base de données grâce à la commande manage.py loaddata fixtures/musicians.json. On pourra aussi et surtout charger ces fichiers lors des tests unitaires, automatiquement.

from django.test import TestCase

class TestMusicians(TestCase):
    fixtures = ['musicians.json']  # Spécifie le fichier de fixture à charger

    def test_playing_guitar(self):
        # Fait quelque chose avec les données chargées depuis la fixture
        pass

Mocks

Lorsqu'on fait des tests unitaires, on doit parfois tester du code qui a une dépendance à quelque chose externe au système : une API, la connexion à une base de données, à un système d'execution de code asynchrone, etc.

Il est important de simuler ces appels extérieurs, pour assurer la fiabilité du test. C'est là qu'on utilise les mocks.

Il existe aussi les stubs, qui sont un concept un peu différent mais souvent confondus. Je vous laisse faire une recherche google sur le sujet. Dans nos exemples, la différence est particulièrement ténue.

Par exemple, on peut mocker une API avec un logiciel du type Wiremock. Le principe est de proposer une API statique, de pré-enregistrer des réponses à des requêtes à l'API externe, et de les retourner lors de chaque appel. Un serveur tourne localement, il suffit alors de changer la configuration du domaine de l'API lors des tests, en lui indiquant l'adresse locale (par exemple http://127.0.0.1:8888).

On peut aussi mocker des méthodes ou des classes directement dans le code, pour éviter d'exécuter du code non désiré pour le test. Par exemple, plutôt que de proposer une vraie fausse API avec Wiremock, on pourrait juste monkeypatcher la méthode get de la librairie request juste avant d'exécuter notre test. C'est-à-dire que l'on va remplacer l'appel de cette méthode par une méthode qui va juste se contenter de retourner des valeurs.

Voici un exemple :

import pytest
import requests

def get_punk_bands():  # La méthode que l'on veut tester
    response = requests.get('https://punkapi.com/api/v1/bands')
    if response.status_code == 200:
        return response.json()
    else:
        return None

def test_get_punk_bands(monkeypatch):
    # Simule la réponse de l'API avec un objet Mock
    mock_response = [{'name': 'The Clash'}, {'name': 'The Ramones'}]
    mock_get = pytest.Mock(return_value=mock_response)
    monkeypatch.setattr(requests, 'get', mock_get)  # Ici, on dit que la méthode get de l'objet request va être remplacée par notre mock

    # Appelle la fonction et vérifie la sortie
    bands = get_punk_bands()
    assert bands == mock_response
    mock_get.assert_called_once_with('https://punkapi.com/api/v1/bands')  # On s'assure que le mock a bien été appelé avec le paramètre attendu

Ainsi, on teste bien notre code, mais on s'arrête à partir du moment où l'on fait appel à un service externe.

Conclusion

Voilà, c'est assez court. Mais si je peux vous donner un conseil : dès que vous avez mis en place votre framework de test (ou vos frameworks si vous voulez utiliser plusieurs niveaux de tests), mettez en place vos outils de fixtures et de mocks (ou assurez-vous que ceux qui viennent avec vos outils fonctionnent bien). Et assurez-vous que vous sachiez les utiliser.

D'expérience, il est assez couteux de mettre en place ces outils en plein milieu d'un projet, surtout dans un contexte avec plusieurs collaborateurs. En effet, il y aura un coût d'adoption qui sera non négligeable, il peut y avoir le besoin de reprendre le code des tests, et ça sera beaucoup de souffrances pour un tout petit sujet. Comme dit le proverbe : "Bloquer sur les mocks, c'est moche." (proverbe provençal)

Et voilà

C'est tout pour aujourd'hui. Le prochain article conclura la série sur l'outillage pour les tests unitaires en abordant les factories.

Tumblr
Pinterest
LinkedIn
WhatsApp

<< Mon retour d'expérience sur le métier de QA >> La qualité dans un projet web, Partie 7 - Tester avec des factories

2022 - tominardi.fr