Accélérer nos tests Python avec la parallélisation

8 min read
Create an image showcasing the concept of parallelization in Python programming with a clear emphasis on database interactions. Visualize a central Python programming language logo, with multiple parallel lines extending from it, each representing a thread of execution. These lines should branch off into stylized icons of databases and servers, indicating the connection and interaction with various data sources. The lines of parallel execution should be interconnected, forming a network that symbolizes efficient data processing and exchange. The background should be a digital landscape, filled with elements like circuit patterns, binary code, and digital waveforms, reflecting a sophisticated programming environment. The overall color scheme should incorporate Python's blue and yellow, creating a vibrant and modern look that highlights the theme of advanced computing in Python.
Create an image showcasing the concept of parallelization in Python programming with a clear emphasis on database interactions. Visualize a central Python programming language logo, with multiple parallel lines extending from it, each representing a thread of execution. These lines should branch off into stylized icons of databases and servers, indicating the connection and interaction with various data sources. The lines of parallel execution should be interconnected, forming a network that symbolizes efficient data processing and exchange. The background should be a digital landscape, filled with elements like circuit patterns, binary code, and digital waveforms, reflecting a sophisticated programming environment. The overall color scheme should incorporate Python's blue and yellow, creating a vibrant and modern look that highlights the theme of advanced computing in Python.

Dans le développement, la mise en place de tests est une étape incontournable.

Qu'ils soient unitaires, fonctionnels ou end-to-end, les tests garantissent la qualité et la fiabilité du code. Cependant, à mesure que notre codebase s'élargit, le nombre de tests augmente, entraînant une augmentation significative du temps de test.

Cette situation a deux conséquences principales :

  • une perte de temps directe pour le développeur.
  • dans le cas de pipelines de CI, des coûts accrus liés au temps d'exécution des tests.

⚠️ Dans cet article, nous allons utiliser SQLlite en tant que base de données, mais cela fonctionne de la même manière si vous utilisez une autre base de données tels Postgres ou MySQL ou autre.

Il suffit de changer l'URL lors de la création de l'engine SQLAlchemy. Par exemple :

Accélérer les tests

Plusieurs stratégies peuvent être adoptées.

Par l'optimisation des tests eux-mêmes, en observant ce qui prend du temps, peut-être même revoir le scope des fixtures etc... Mais cette approche a plusieurs limites, dont la première : si le code à tester est long, malgré l'optimisation des tests, il y aura peu de différence.

La méthode la plus simple et la plus efficace semble être la parallélisation des tests. L'idée est de lancer plusieurs tests simultanément au lieu de les exécuter les uns aprés les autres.


Parallélisation avec Pytest-xdist

Pytest-xdist est un outil Python qui facilite cette parallélisation. L'utilisation de cet outil est simple : si vous disposez de 32 CPU, vous pouvez théoriquement exécuter jusqu'à 32 tests simultanément.

Cette capacité à exécuter des tests en parallèle peut grandement améliorer le temps nécessaire pour lancer votre suite de tests.

De plus, la mise en place est très facile :

text
>> pip install pytest-xdist

>> pytest -n auto

La commande pytest -n auto va détecter automatiquement votre nombre de CPU afin de pouvoir créer le nombre approprié de workers ,et démarrer les tests sur ceux-ci.


Les side-effects et la parallélisation

Cependant, la parallélisation soulève un défi de taille : la gestion des side effects. Certains tests peuvent modifier des ressources externes, comme des fichiers ou des bases de données, polluant ainsi les l'environnements pour d'autres tests.

Par exemple, un test créant des instances dans une base de données peut laisser des traces qui affecteront les tests suivants.

C'est un problème connu : lorsqu'on execute des tests qui se servent d'une base de données, nous devons nettoyer cette base de données entre chaque test.


Nettoyer sa base de donnée après chaque test

Pour remédier à cela, deux approches principales sont souvent utilisées :

  • l'utilisation de rollbacks en fin de test
  • la suppression de la base de données après chaque test.

Afin d'illustrer nos exemples, nous allons partir du model SQLAlchemy que nous désirons tester :


1) Tests sans nettoyage des données

Nous avons deux tests : un qui crée un Book et qui vérifie qu'il existe bien un seul Book; un second test qui crée 5 Books et qui vérifie qu'il en existe 5. Ces tests sont simples mais ils vont permettre d'illustrer nos propos.

Si j'execute de manière séquentielle (l'un aprés l'autre) mes tests, je vais d'abord créer 1 Book . Le test va passer. Mais lors du deuxieme test, je vais créer 5 books supplémentaires, sans avoir nettoyé ma base de données, en supprimant le book de l'ancien test. Par conséquent, au lieu d'avoir 5 books j'en aurai 6.


Donc, il nous faut un moyen de remettre notre database en état entre chaque test pour éviter les pollutions.


2.1) Utilisation de rollback à la fin du test

Afin d'éviter la pollution de test, une solution souvent préconisée est de réaliser un "rollback" à la fin de notre test, de sorte que toutes les actions réalisées dans la transaction courante soient annulées.


On peut donc ajouter ce comportement directement à notre fixture.

Essayons ce code ci :

Voici les résultats de ces tests :

Comme vous pouvez le constater, les tests ne passent pas. Pourquoi ?

Tout simplement parce que, même si nous faisons un "rollback" à la fin du test dans notre test, apparaissent des commits pour seeder nos tests, et donc ces changements sont persistés en base de données au moment du commit.

Ainsi, dès que le test fait un commit, le rollback n'aura plus d'effet car les résultats auront déjà été persisté en base de données.


2.2) Un hack pour rollback

Il existe effectivement un "hack" pour rollback entre chaque test.

Mais ceci dépasse le scope de cet article. Je vous renvoie à l'article qui l'aborde en cliquant ici.

Pour plus de détails, vous pouvez directement vous rendre sur la page de SQLAlchemy qui détaille son fonctionnement en suivant ce lien.


Si vous souhaitez simplement mettre en place le rollback dans vos tests, je vous transmets ci-dessous la fixture telle qu'elle est préconisée dans l'article :



Si vous appréciez la newsletter StuffAndCode, vous pouvez vous inscrire afin de ne manquer aucun article !

🐍 Et recevez en PLUS, un article exclusif sur comment profiler n'importe quel type de code Python.

Abonnez-vous maintenant



  1. Drop la base de données

Pourquoi ne pas tout simplement créer et supprimer la base de données lors de chaque test ? Ca nous permettrait d'être sûr d'avoir un environnement propre !

Nous pouvons mettre en place ce mécanisme comme vous pouvez le voir ci-dessous.


Nos tests passent, c'est bien. Mais à cette approche, incombent certains problemes.


3.1) Les problématiques de cette approche

Supprimer et recréer la database pour chaque test prend du temps, et cela peut rallonger considérablement le temps d'execution de vos tests.

Si vous parallelisez vos tests, cela veut également dire que lorsqu'un test va terminer, il va modifier les données et polluer un autre test, également drop la base de données, pendant que les autres tests finissent de s'executer. Ce qui va générer des erreurs:


3.2) La Solution : Créer une base de données par worker

Lors de l'utilisation de Pytest-xdist, un nouveau problème émerge : si chaque test parallélisé tente de modifier la même base de données, des conflits surviendront inévitablement.

La solution réside dans la modification de notre fixture de base de données pour créer une base distincte pour chaque worker . Ainsi, chaque worker opère sur sa propre base de données, et évite les conflits tout en bénéficiant d**'un véritable parallélisme**.

Pour ce faire, pytest-xdist ajoute une variable d'environnement "PYTEST_XDIST_WORKER" qui attribue une valeur pour chaque worker. Par exemple, si nous avons 10 workers, chaque worker aura une variable PYTEST_XDIST_WORKER qui vaudra 0, 1, 2 ... 9.

Nous pouvons donc créer une base de données pour chaque worker en nous servant de cette variable d'environnement, résultant en un environnement isolé pour chaque worker.


Cela fonctionne !


Cependant, vous pouvez noter que le temps est tout de même passé de +3s contre 0.23s pour la version séquentielle. La raison ? Démarrer les tests sur plusieurs CPU prend du temp s !

Donc si vos tests sont déjà trés rapides de manière séquentielle, il est peut-être plus pertinent d'éviter de mettre en place la parallélisation.

A l'inverse, si vos tests sont plus longs, le gain de temps peut-être vraiment très intéressant.


Quizz Time

  • Quelle librairie utilisons-nous pour paralleliser nos tests ?
  • Dans quel cas est-il préférable de ne pas paralleliser les tests ?
  • Quels sont les avantages et inconvénients de drop la database entre chaque test ?


Si vous appréciez la newsletter StuffAndCode, vous pouvez vous inscrire afin de ne manquer aucun article !

🐍 Et recevez en PLUS, un article exclusif sur comment profiler n'importe quel type de code Python.

Abonnez-vous maintenant


Written by

Copyright © 2026Stuff And CodePowered by Writizzy