Premiers pas avec les tests en Python

Premiers pas avec les tests en Python

Ce didacticiel s'adresse à tous ceux qui ont écrit une application fantastique en Python mais qui n'ont pas encore écrit de tests.

Les tests en Python sont un sujet énorme et peuvent venir avec beaucoup de complexité, mais cela n'a pas besoin d'être difficile. Vous pouvez commencer à créer des tests simples pour votre application en quelques étapes simples, puis à partir de là.

Dans ce didacticiel, vous apprendrez à créer un test de base, à l'exécuter et à trouver les bogues avant que vos utilisateurs ne le fassent! Vous découvrirez les outils disponibles pour écrire et exécuter des tests, vérifier les performances de votre application et même rechercher des problèmes de sécurité.

Free Bonus:5 Thoughts On Python Mastery, un cours gratuit pour les développeurs Python qui vous montre la feuille de route et l'état d'esprit dont vous aurez besoin pour faire passer vos compétences Python au niveau supérieur.

Tester votre code

Il existe de nombreuses façons de tester votre code. Dans ce didacticiel, vous apprendrez les techniques des étapes les plus élémentaires et évoluerez vers des méthodes avancées.

Automatisé vs Test manuel

La bonne nouvelle est que vous avez probablement déjà créé un test sans vous en rendre compte. Rappelez-vous quand vous avez exécuté votre application et l'avez utilisée pour la première fois? Avez-vous vérifié les fonctionnalités et expérimenté leur utilisation? C'est ce qu'on appelleexploratory testing et c'est une forme de test manuel.

Les tests exploratoires sont une forme de tests qui sont effectués sans plan. Dans un test exploratoire, vous explorez simplement l'application.

Pour disposer d'un ensemble complet de tests manuels, il vous suffit de dresser une liste de toutes les fonctionnalités de votre application, des différents types d'entrées qu'elle peut accepter et des résultats attendus. Maintenant, chaque fois que vous modifiez votre code, vous devez parcourir chaque élément de cette liste et le vérifier.

Cela ne semble pas très amusant, n'est-ce pas?

C'est là qu'interviennent les tests automatisés. Les tests automatisés sont l'exécution de votre plan de test (les parties de votre application que vous souhaitez tester, l'ordre dans lequel vous souhaitez les tester et les réponses attendues) par un script au lieu d'un humain. Python est déjà livré avec un ensemble d'outils et de bibliothèques pour vous aider à créer des tests automatisés pour votre application. Nous allons explorer ces outils et bibliothèques dans ce didacticiel.

Tests unitaires vs Tests d'intégration

Le monde des tests ne manque pas de terminologie, et maintenant que vous connaissez la différence entre les tests automatisés et les tests manuels, il est temps d'aller plus loin.

Pensez à la façon dont vous pourriez tester les lumières d'une voiture. Vous allumez les lumières (connues sous le nom detest step) et sortez de la voiture ou demandez à un ami de vérifier que les lumières sont allumées (appeléestest assertion). Le test de plusieurs composants est appeléintegration testing.

Pensez à tout ce qui doit fonctionner correctement pour qu'une tâche simple donne le bon résultat. Ces composants sont comme les parties de votre application, toutes ces classes, fonctions et modules que vous avez écrits.

Un défi majeur avec les tests d'intégration est lorsqu'un test d'intégration ne donne pas le bon résultat. Il est très difficile de diagnostiquer le problème sans pouvoir isoler quelle partie du système est défaillante. Si les lumières ne se sont pas allumées, alors peut-être que les ampoules sont cassées. La batterie est-elle morte? Et l'alternateur? L'ordinateur de la voiture tombe-t-il en panne?

Si vous avez une voiture moderne de luxe, elle vous dira quand vos ampoules auront disparu. Il le fait en utilisant une forme deunit test.

Un test unitaire est un test plus petit, qui vérifie qu'un seul composant fonctionne correctement. Un test unitaire vous aide à isoler ce qui est cassé dans votre application et à le réparer plus rapidement.

Vous venez de voir deux types de tests:

  1. Un test d'intégration vérifie que les composants de votre application fonctionnent ensemble.

  2. Un test unitaire vérifie un petit composant de votre application.

Vous pouvez écrire des tests d'intégration et des tests unitaires en Python. Pour écrire un test unitaire pour la fonction intégréesum(), vous devez vérifier la sortie desum() par rapport à une sortie connue.

Par exemple, voici comment vérifier que lesum() des nombres(1, 2, 3) est égal à6:

>>>

>>> assert sum([1, 2, 3]) == 6, "Should be 6"

Cela ne produira rien sur le REPL car les valeurs sont correctes.

Si le résultat desum() est incorrect, cela échouera avec unAssertionError et le message"Should be 6". Essayez à nouveau une instruction d'assertion avec des valeurs incorrectes pour voir unAssertionError:

>>>

>>> assert sum([1, 1, 1]) == 6, "Should be 6"
Traceback (most recent call last):
  File "", line 1, in 
AssertionError: Should be 6

Dans le REPL, vous voyez lesAssertionError élevés car le résultat desum() ne correspond pas à6.

Au lieu de tester sur le REPL, vous voudrez le mettre dans un nouveau fichier Python appelétest_sum.py et l'exécuter à nouveau:

def test_sum():
    assert sum([1, 2, 3]) == 6, "Should be 6"

if __name__ == "__main__":
    test_sum()
    print("Everything passed")

Vous avez maintenant écrit untest case, une assertion et un point d'entrée (la ligne de commande). Vous pouvez maintenant l'exécuter sur la ligne de commande:

$ python test_sum.py
Everything passed

Vous pouvez voir le résultat réussi,Everything passed.

En Python,sum() accepte tout itérable comme premier argument. Vous avez testé avec une liste. Maintenant, testez également avec un tuple. Créez un nouveau fichier appelétest_sum_2.py avec le code suivant:

def test_sum():
    assert sum([1, 2, 3]) == 6, "Should be 6"

def test_sum_tuple():
    assert sum((1, 2, 2)) == 6, "Should be 6"

if __name__ == "__main__":
    test_sum()
    test_sum_tuple()
    print("Everything passed")

Lorsque vous exécuteztest_sum_2.py, le script donnera une erreur car lesum() de(1, 2, 2) est5, pas6. Le résultat du script vous donne le message d'erreur, la ligne de code et le traceback:

$ python test_sum_2.py
Traceback (most recent call last):
  File "test_sum_2.py", line 9, in 
    test_sum_tuple()
  File "test_sum_2.py", line 5, in test_sum_tuple
    assert sum((1, 2, 2)) == 6, "Should be 6"
AssertionError: Should be 6

Ici, vous pouvez voir comment une erreur dans votre code donne une erreur sur la console avec quelques informations sur où était l'erreur et quel était le résultat attendu.

L'écriture de tests de cette manière est acceptable pour une vérification simple, mais que se passe-t-il si plus d'un échoue? C'est là que les testeurs entrent en jeu. Le testeur est une application spéciale conçue pour exécuter des tests, vérifier la sortie et vous fournir des outils pour déboguer et diagnostiquer les tests et les applications.

Choisir un testeur

Il existe de nombreux exécuteurs de test disponibles pour Python. Celui intégré à la bibliothèque standard Python s'appelleunittest. Dans ce didacticiel, vous utiliserez les cas de testunittest et le lanceur de testunittest. Les principes deunittest sont facilement transférables vers d'autres frameworks. Les trois coureurs de test les plus populaires sont:

  • unittest

  • nose ounose2

  • pytest

Il est important de choisir le meilleur testeur pour vos besoins et votre niveau d'expérience.

unittest

unittest est intégré à la bibliothèque standard Python depuis la version 2.1. Vous le verrez probablement dans les applications Python commerciales et les projets open source.

unittest contient à la fois un cadre de test et un exécuteur de test. unittest a des exigences importantes pour écrire et exécuter des tests.

unittest requiert que:

  • Vous mettez vos tests en classes comme méthodes

  • Vous utilisez une série de méthodes d'assertion spéciales dans la classeunittest.TestCase au lieu de l'instructionassert intégrée

Pour convertir l'exemple précédent en cas de testunittest, vous devez:

  1. Importerunittest depuis la bibliothèque standard

  2. Créez une classe appeléeTestSum qui hérite de la classeTestCase

  3. Convertissez les fonctions de test en méthodes en ajoutantself comme premier argument

  4. Modifiez les assertions pour utiliser la méthodeself.assertEqual() sur la classeTestCase

  5. Modifiez le point d'entrée de la ligne de commande pour appelerunittest.main()

Suivez ces étapes en créant un nouveau fichiertest_sum_unittest.py avec le code suivant:

import unittest


class TestSum(unittest.TestCase):

    def test_sum(self):
        self.assertEqual(sum([1, 2, 3]), 6, "Should be 6")

    def test_sum_tuple(self):
        self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")

if __name__ == '__main__':
    unittest.main()

Si vous l'exécutez sur la ligne de commande, vous verrez un succès (indiqué par.) et un échec (indiqué parF):

$ python test_sum_unittest.py
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_sum_unittest.py", line 9, in test_sum_tuple
    self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Vous venez d'exécuter deux tests en utilisant le lanceur de testsunittest.

Note: Soyez prudent si vous écrivez des cas de test qui doivent s'exécuter à la fois en Python 2 et 3. En Python 2.7 et versions antérieures,unittest est appeléunittest2. Si vous importez simplement deunittest, vous obtiendrez différentes versions avec des fonctionnalités différentes entre Python 2 et 3.

Pour plus d'informations surunittest, vous pouvez explorer lesunittest Documentation.

nose

Vous constaterez peut-être qu'avec le temps, à mesure que vous écrivez des centaines, voire des milliers de tests pour votre application, il devient de plus en plus difficile de comprendre et d'utiliser la sortie deunittest.

nose est compatible avec tous les tests écrits à l'aide du frameworkunittest et peut être utilisé en remplacement du lanceur de testunittest. Le développement denose en tant qu'application open source a pris du retard et un fork appelénose2 a été créé. Si vous partez de zéro, il est recommandé d’utilisernose2 au lieu denose.

Pour démarrer avecnose2, installeznose2 depuis PyPI et exécutez-le sur la ligne de commande. nose2 essaiera de découvrir tous les scripts de test nomméstest*.py et les cas de test héritant deunittest.TestCase dans votre répertoire actuel:

$ pip install nose2
$ python -m nose2
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_sum_unittest.py", line 9, in test_sum_tuple
    self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Vous venez d'exécuter le test que vous avez créé danstest_sum_unittest.py à partir du lanceur de testnose2. nose2 propose de nombreux indicateurs de ligne de commande pour filtrer les tests que vous exécutez. Pour plus d'informations, vous pouvez explorer lesNose 2 documentation.

pytest

pytest prend en charge l'exécution des cas de testunittest. Le véritable avantage depytest vient de l'écriture des cas de testpytest. Les cas de testpytest sont une série de fonctions dans un fichier Python commençant par le nomtest_.

pytest possède d'autres fonctionnalités intéressantes:

  • Prise en charge de l'instructionassert intégrée au lieu d'utiliser des méthodes spécialesself.assert*()

  • Prise en charge du filtrage des cas de test

  • Possibilité de relancer le dernier test ayant échoué

  • Un écosystème de centaines de plugins pour étendre les fonctionnalités

L'écriture de l'exemple de cas de testTestSum pourpytest ressemblerait à ceci:

def test_sum():
    assert sum([1, 2, 3]) == 6, "Should be 6"

def test_sum_tuple():
    assert sum((1, 2, 2)) == 6, "Should be 6"

Vous avez supprimé lesTestCase, toute utilisation de classes et le point d'entrée de la ligne de commande.

Plus d'informations peuvent être trouvées dans lesPytest Documentation Website.

Écrire votre premier test

Regroupons ce que vous avez appris jusqu'à présent et, au lieu de tester la fonctionsum() intégrée, testons une implémentation simple de la même exigence.

Créez un nouveau dossier de projet et, à l'intérieur, créez un nouveau dossier appelémy_sum. À l'intérieur demy_sum, créez un fichier vide appelé__init__.py. La création du fichier__init__.py signifie que le dossiermy_sum peut être importé en tant que module à partir du répertoire parent.

Votre dossier de projet devrait ressembler à ceci:

project/
│
└── my_sum/
    └── __init__.py

Ouvrezmy_sum/__init__.py et créez une nouvelle fonction appeléesum(), qui prend un itérable (une liste, un tuple ou un ensemble) et ajoute les valeurs ensemble:

def sum(arg):
    total = 0
    for val in arg:
        total += val
    return total

Cet exemple de code crée une variable appeléetotal, itère sur toutes les valeurs dearg et les ajoute àtotal. Il renvoie ensuite le résultat une fois l'itérable épuisé.

Où écrire le test

Pour commencer à écrire des tests, vous pouvez simplement créer un fichier appelétest.py, qui contiendra votre premier cas de test. Comme le fichier devra pouvoir importer votre application pour pouvoir la tester, vous voulez placertest.py au-dessus du dossier du package, de sorte que votre arborescence de répertoires ressemblera à ceci:

project/
│
├── my_sum/
│   └── __init__.py
|
└── test.py

Vous constaterez que, à mesure que vous ajoutez de plus en plus de tests, votre fichier unique deviendra encombré et difficile à gérer, vous pourrez donc créer un dossier appelétests/ et diviser les tests en plusieurs fichiers. Il est conventionnel de s'assurer que chaque fichier commence partest_ afin que tous les exécuteurs de test supposent que le fichier Python contient des tests à exécuter. Certains très gros projets divisent les tests en plusieurs sous-répertoires en fonction de leur objectif ou de leur utilisation.

Note: Que faire si votre application est un seul script?

Vous pouvez importer tous les attributs du script, tels que les classes, les fonctions et les variables à l'aide de la fonction__import__() intégrée. Au lieu defrom my_sum import sum, vous pouvez écrire ce qui suit:

target = __import__("my_sum.py")
sum = target.sum

L’avantage de l’utilisation de__import__() est que vous n’avez pas besoin de transformer le dossier de votre projet en package et que vous pouvez spécifier le nom du fichier. Cela est également utile si votre nom de fichier entre en collision avec des packages de bibliothèque standard. Par exemple,math.py entrerait en collision avec le modulemath.

Comment structurer un test simple

Avant de vous lancer dans la rédaction de tests, vous devez d'abord prendre quelques décisions:

  1. Que voulez-vous tester?

  2. Ecrivez-vous un test unitaire ou un test d'intégration?

Ensuite, la structure d'un test devrait suivre de manière lâche ce flux de travail:

  1. Créez vos entrées

  2. Exécuter le code testé, capturer la sortie

  3. Comparer la sortie avec un résultat attendu

Pour cette application, vous testezsum(). Il existe de nombreux comportements danssum() que vous pouvez vérifier, tels que:

  • Peut-il additionner une liste de nombres entiers (entiers)?

  • Peut-il additionner un tuple ou un ensemble?

  • Peut-il additionner une liste de flotteurs?

  • Que se passe-t-il lorsque vous lui fournissez une valeur incorrecte, comme un seul entier ou une chaîne?

  • Que se passe-t-il lorsque l'une des valeurs est négative?

Le test le plus simple serait une liste d'entiers. Créez un fichier,test.py avec le code Python suivant:

import unittest

from my_sum import sum


class TestSum(unittest.TestCase):
    def test_list_int(self):
        """
        Test that it can sum a list of integers
        """
        data = [1, 2, 3]
        result = sum(data)
        self.assertEqual(result, 6)

if __name__ == '__main__':
    unittest.main()

Cet exemple de code:

  1. Importesum() à partir du packagemy_sum que vous avez créé

  2. Définit une nouvelle classe de cas de test appeléeTestSum, qui hérite deunittest.TestCase

  3. Définit une méthode de test,.test_list_int(), pour tester une liste d'entiers. La méthode.test_list_int() va:

    • Déclarez une variabledata avec une liste de nombres(1, 2, 3)

    • Affectez le résultat demy_sum.sum(data) à une variableresult

    • Affirmez que la valeur deresult est égale à6 en utilisant la méthode.assertEqual() sur la classeunittest.TestCase

  4. Définit un point d'entrée de ligne de commande, qui exécute le test-runnerunittest.main()

Si vous ne savez pas ce qu'estself ou comment.assertEqual() est défini, vous pouvez rafraîchir votre programmation orientée objet avecPython 3 Object-Oriented Programming.

Comment écrire des assertions

La dernière étape de l'écriture d'un test consiste à valider la sortie par rapport à une réponse connue. Ceci est connu sous le nom deassertion. Il existe quelques bonnes pratiques générales sur la façon d'écrire des assertions:

  • Assurez-vous que les tests sont répétables et exécutez votre test plusieurs fois pour vous assurer qu'il donne le même résultat à chaque fois

  • Essayez d'affirmer des résultats qui se rapportent à vos données d'entrée, comme vérifier que le résultat est la somme réelle des valeurs dans l'exemplesum()

unittest est livré avec de nombreuses méthodes pour affirmer les valeurs, les types et l'existence des variables. Voici quelques-unes des méthodes les plus couramment utilisées:

Méthode Équivalent à

.assertEqual(a, b)

a == b

.assertTrue(x)

bool(x) is True

.assertFalse(x)

bool(x) is False

.assertIs(a, b)

a is b

.assertIsNone(x)

x is None

.assertIn(a, b)

a in b

.assertIsInstance(a, b)

isinstance(a, b)

.assertIs(),.assertIsNone(),.assertIn() et.assertIsInstance() ont tous des méthodes opposées, nommées.assertIsNot(), et ainsi de suite.

Effets secondaires

Lorsque vous écrivez des tests, ce n'est souvent pas aussi simple que de regarder la valeur de retour d'une fonction. Souvent, l'exécution d'un morceau de code modifiera d'autres choses dans l'environnement, comme l'attribut d'une classe, un fichier sur le système de fichiers ou une valeur dans une base de données. Celles-ci sont appeléesside effects et constituent une partie importante des tests. Décidez si l'effet secondaire est testé avant de l'inclure dans votre liste d'assertions.

Si vous trouvez que l'unité de code que vous souhaitez tester a de nombreux effets secondaires, vous risquez de casser lesSingle Responsibility Principle. Briser le principe de responsabilité unique signifie que le morceau de code fait trop de choses et qu'il vaudrait mieux qu'il soit refactorisé. Suivre le principe de responsabilité unique est un excellent moyen de concevoir du code qui permet d'écrire facilement des tests unitaires reproductibles et simples pour, et finalement, des applications fiables.

Exécution de votre premier test

Maintenant que vous avez créé le premier test, vous souhaitez l'exécuter. Bien sûr, vous savez que cela va réussir, mais avant de créer des tests plus complexes, vous devez vérifier que vous pouvez exécuter les tests avec succès.

Exécution de testeurs

L'application Python qui exécute votre code de test, vérifie les assertions et vous donne les résultats des tests dans votre console s'appelletest runner.

Au bas detest.py, vous avez ajouté ce petit extrait de code:

if __name__ == '__main__':
    unittest.main()

Il s'agit d'un point d'entrée de ligne de commande. Cela signifie que si vous exécutez le script seul en exécutantpython test.py sur la ligne de commande, il appelleraunittest.main(). Cela exécute le testeur en découvrant toutes les classes de ce fichier qui héritent deunittest.TestCase.

C'est l'une des nombreuses manières d'exécuter le testeur deunittest. Lorsque vous avez un seul fichier de test nommétest.py, appelerpython test.py est un excellent moyen de commencer.

Une autre façon consiste à utiliser la ligne de commandeunittest. Essaye ça:

$ python -m unittest test

Cela exécutera le même module de test (appelétest) via la ligne de commande.

Vous pouvez fournir des options supplémentaires pour modifier la sortie. L'un de ceux-ci est-v pour verbeux. Essayez cela ensuite:

$ python -m unittest -v test
test_list_int (test.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 tests in 0.000s

Cela a exécuté le test unique à l'intérieur detest.py et a imprimé les résultats sur la console. Le mode détaillé répertorie les noms des tests qu'il a exécutés en premier, ainsi que le résultat de chaque test.

Au lieu de fournir le nom d'un module contenant des tests, vous pouvez demander une découverte automatique à l'aide de ce qui suit:

$ python -m unittest discover

Cela recherchera dans le répertoire courant tous les fichiers nomméstest*.py et tentera de les tester.

Une fois que vous avez plusieurs fichiers de test, tant que vous suivez le modèle de dénomination detest*.py, vous pouvez fournir le nom du répertoire à la place en utilisant l'indicateur-s et le nom du répertoire:

$ python -m unittest discover -s tests

unittest exécutera tous les tests dans un seul plan de test et vous donnera les résultats.

Enfin, si votre code source n'est pas à la racine du répertoire et contenu dans un sous-répertoire, par exemple dans un dossier appelésrc/, vous pouvez indiquer àunittest où exécuter les tests afin qu'il puisse importer les modules correctement avec l'indicateur-t:

$ python -m unittest discover -s tests -t src

unittest passera dans le répertoiresrc/, recherchera tous les fichierstest*.py dans le répertoiretests et les exécutera.

Comprendre la sortie de test

C'était un exemple très simple où tout se passe, alors maintenant vous allez essayer un test qui échoue et interpréter la sortie.

sum() devrait pouvoir accepter d'autres listes de types numériques, comme les fractions.

En haut du fichiertest.py, ajoutez une instruction d'importation pour importer le typeFraction depuis le modulefractions dans la bibliothèque standard:

from fractions import Fraction

Ajoutez maintenant un test avec une assertion qui attend la valeur incorrecte, dans ce cas, en supposant que la somme de 1/4, 1/4 et 2/5 soit 1:

import unittest

from my_sum import sum


class TestSum(unittest.TestCase):
    def test_list_int(self):
        """
        Test that it can sum a list of integers
        """
        data = [1, 2, 3]
        result = sum(data)
        self.assertEqual(result, 6)

    def test_list_fraction(self):
        """
        Test that it can sum a list of fractions
        """
        data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
        result = sum(data)
        self.assertEqual(result, 1)

if __name__ == '__main__':
    unittest.main()

Si vous exécutez à nouveau les tests avecpython -m unittest test, vous devriez voir la sortie suivante:

$ python -m unittest test
F.
======================================================================
FAIL: test_list_fraction (test.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 21, in test_list_fraction
    self.assertEqual(result, 1)
AssertionError: Fraction(9, 10) != 1

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Dans la sortie, vous verrez les informations suivantes:

  1. La première ligne affiche les résultats de l'exécution de tous les tests, l'un a échoué (F) et l'autre réussi (.).

  2. L'entréeFAIL affiche quelques détails sur l'échec du test:

    • Le nom de la méthode de test (test_list_fraction)

    • Le module de test (test) et le cas de test (TestSum)

    • Un retour sur la ligne défaillante

    • Les détails de l'assertion avec le résultat attendu (1) et le résultat réel (Fraction(9, 10))

N'oubliez pas que vous pouvez ajouter des informations supplémentaires à la sortie du test en ajoutant l'indicateur-v à la commandepython -m unittest.

Exécution de vos tests à partir de PyCharm

Si vous utilisez l'EDI PyCharm, vous pouvez exécuterunittest oupytest en suivant ces étapes:

  1. Dans la fenêtre de l'outil Projet, sélectionnez le répertoiretests.

  2. Dans le menu contextuel, choisissez la commande d'exécution pourunittest. Par exemple, choisissezRun ‘Unittests in my Tests…’.

Cela exécuteraunittest dans une fenêtre de test et vous donnera les résultats dans PyCharm:

PyCharm Testing

Plus d'informations sont disponibles sur lesPyCharm Website.

Exécution de vos tests à partir du code Visual Studio

Si vous utilisez l'EDI Microsoft Visual Studio Code, la prise en charge de l'exécution deunittest,nose etpytest est intégrée au plug-in Python.

Si vous avez installé le plugin Python, vous pouvez configurer la configuration de vos tests en ouvrant la palette de commandes avecCtrl[.kbd .key-shift]##Shift##[.kbd .key-p]#P # et en tapant «Python test». Vous verrez une gamme d'options:

Visual Studio Code Step 1

ChoisissezDebug All Unit Tests, et VSCode lèvera alors une invite pour configurer le cadre de test. Cliquez sur le rouage pour sélectionner le lanceur de test (unittest) et le répertoire de base (.).

Une fois que cela est configuré, vous verrez l'état de vos tests en bas de la fenêtre, et vous pouvez accéder rapidement aux journaux de test et relancer les tests en cliquant sur ces icônes:

Visual Studio Code Step 2

Cela montre que les tests sont en cours d'exécution, mais certains échouent.

Test de cadres Web comme Django et Flask

Si vous écrivez des tests pour une application Web en utilisant l'un des frameworks populaires comme Django ou Flask, il existe des différences importantes dans la façon dont vous écrivez et exécutez les tests.

Pourquoi ils sont différents des autres applications

Pensez à tout le code que vous allez tester dans une application Web. Les itinéraires, les vues et les modèles nécessitent tous beaucoup d'importations et de connaissances sur les cadres utilisés.

Ceci est similaire au test de voiture au début du didacticiel: vous devez démarrer l'ordinateur de la voiture avant de pouvoir exécuter un test simple comme vérifier les lumières.

Django et Flask vous facilitent la tâche en fournissant un cadre de test basé surunittest. Vous pouvez continuer à écrire des tests de la manière que vous avez apprise, mais les exécuter légèrement différemment.

Comment utiliser le Django Test Runner

Le template de Djangostartapp aura créé un fichiertests.py dans le répertoire de votre application. Si vous ne l'avez pas déjà, vous pouvez le créer avec le contenu suivant:

from django.test import TestCase

class MyTestCase(TestCase):
    # Your test methods

La principale différence avec les exemples jusqu'à présent est que vous devez hériter dedjango.test.TestCase au lieu deunittest.TestCase. Ces classes ont la même API, mais la classe DjangoTestCase met en place tous les états requis pour tester.

Pour exécuter votre suite de tests, au lieu d'utiliserunittest en ligne de commande, vous utilisezmanage.py test:

$ python manage.py test

Si vous voulez plusieurs fichiers de test, remplaceztests.py par un dossier appelétests, insérez un fichier vide à l'intérieur appelé__init__.py et créez vos fichierstest_*.py. Django va les découvrir et les exécuter.

Plus d'informations sont disponibles sur lesDjango Documentation Website.

Comment utiliserunittest et Flask

Flask nécessite que l'application soit importée puis mise en mode test. Vous pouvez instancier un client de test et utiliser le client de test pour effectuer des requêtes vers toutes les routes de votre application.

Toute l'instanciation du client de test est effectuée dans la méthodesetUp de votre scénario de test. Dans l'exemple suivant,my_app est le nom de l'application. Ne vous inquiétez pas si vous ne savez pas ce que faitsetUp. Vous en apprendrez plus à ce sujet dans la sectionMore Advanced Testing Scenarios.

Le code dans votre fichier de test devrait ressembler à ceci:

import my_app
import unittest


class MyTestCase(unittest.TestCase):

    def setUp(self):
        my_app.app.testing = True
        self.app = my_app.app.test_client()

    def test_home(self):
        result = self.app.get('/')
        # Make your assertions

Vous pouvez ensuite exécuter les cas de test à l'aide de la commandepython -m unittest discover.

Plus d'informations sont disponibles sur lesFlask Documentation Website.

Scénarios de test plus avancés

Avant de vous lancer dans la création de tests pour votre application, n'oubliez pas les trois étapes de base de chaque test:

  1. Créez vos entrées

  2. Exécuter le code, capturer la sortie

  3. Comparer la sortie avec un résultat attendu

Ce n'est pas toujours aussi simple que de créer une valeur statique pour l'entrée comme une chaîne ou un nombre. Parfois, votre application nécessite une instance d'une classe ou d'un contexte. Que faites-vous alors?

Les données que vous créez en tant qu'entrée sont appeléesfixture. Il est courant de créer des appareils et de les réutiliser.

Si vous exécutez le même test et que vous transmettez des valeurs différentes à chaque fois et que vous attendez le même résultat, cela s'appelleparameterization.

Gestion des échecs attendus

Auparavant, lorsque vous faisiez une liste de scénarios pour testersum(), une question se posait: que se passe-t-il lorsque vous lui fournissez une valeur incorrecte, comme un seul entier ou une chaîne?

Dans ce cas, vous vous attendez à ce quesum() renvoie une erreur. Lorsqu'il déclenche une erreur, le test échoue.

Il existe un moyen spécial de gérer les erreurs attendues. Vous pouvez utiliser.assertRaises() comme gestionnaire de contexte, puis à l'intérieur du blocwith exécutez les étapes de test:

import unittest

from my_sum import sum


class TestSum(unittest.TestCase):
    def test_list_int(self):
        """
        Test that it can sum a list of integers
        """
        data = [1, 2, 3]
        result = sum(data)
        self.assertEqual(result, 6)

    def test_list_fraction(self):
        """
        Test that it can sum a list of fractions
        """
        data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
        result = sum(data)
        self.assertEqual(result, 1)

    def test_bad_type(self):
        data = "banana"
        with self.assertRaises(TypeError):
            result = sum(data)

if __name__ == '__main__':
    unittest.main()

Ce cas de test ne réussira désormais que sisum(data) lève unTypeError. Vous pouvez remplacerTypeError par n'importe quel type d'exception de votre choix.

Isoler les comportements dans votre application

Plus tôt dans le didacticiel, vous avez appris ce qu'est un effet secondaire. Les effets secondaires rendent le test unitaire plus difficile car, chaque fois qu'un test est exécuté, il peut donner un résultat différent, ou pire encore, un test peut avoir un impact sur l'état de l'application et entraîner l'échec d'un autre test!

Testing Side Effects

Il existe quelques techniques simples que vous pouvez utiliser pour tester des parties de votre application qui ont de nombreux effets secondaires:

  • Refactorisation du code pour suivre le principe de responsabilité unique

  • Se moquer de tout appel de méthode ou de fonction pour supprimer les effets secondaires

  • Utilisation de tests d'intégration au lieu de tests unitaires pour cette partie de l'application

Si vous n’êtes pas familier avec les moqueries, consultezPython CLI Testing pour quelques bons exemples.

Écriture de tests d'intégration

Jusqu'à présent, vous avez appris principalement sur les tests unitaires. Les tests unitaires sont un excellent moyen de créer du code prévisible et stable. Mais à la fin de la journée, votre application doit fonctionner lorsqu'elle démarre!

Le test d'intégration est le test de plusieurs composants de l'application pour vérifier qu'ils fonctionnent ensemble. Les tests d'intégration peuvent nécessiter d'agir comme un consommateur ou un utilisateur de l'application en:

  • Appel d'une API HTTP REST

  • Appel d'une API Python

  • Appeler un service Web

  • Exécuter une ligne de commande

Chacun de ces types de tests d'intégration peut être écrit de la même manière qu'un test unitaire, en suivant le modèle Input, Execute et Assert. La différence la plus significative est que les tests d'intégration vérifient plus de composants à la fois et auront donc plus d'effets secondaires qu'un test unitaire. De plus, les tests d'intégration nécessiteront la mise en place de plusieurs appareils, comme une base de données, une prise réseau ou un fichier de configuration.

C'est pourquoi il est recommandé de séparer vos tests unitaires et vos tests d'intégration. La création des fixtures nécessaires à une intégration comme une base de données de test et les cas de test eux-mêmes prennent souvent beaucoup plus de temps à exécuter que les tests unitaires, vous pouvez donc ne vouloir exécuter des tests d'intégration avant de passer en production qu'au lieu d'une fois à chaque validation.

Un moyen simple de séparer les tests unitaires et d'intégration consiste simplement à les placer dans différents dossiers:

project/
│
├── my_app/
│   └── __init__.py
│
└── tests/
    |
    ├── unit/
    |   ├── __init__.py
    |   └── test_sum.py
    |
    └── integration/
        ├── __init__.py
        └── test_integration.py

Il existe de nombreuses façons d'exécuter uniquement un groupe sélectionné de tests. L'indicateur de répertoire source spécifié,-s, peut être ajouté àunittest discover avec le chemin contenant les tests:

$ python -m unittest discover -s tests/integration

unittest vous aura donné les résultats de tous les tests dans le répertoiretests/integration.

Test des applications pilotées par les données

De nombreux tests d'intégration nécessiteront des données dorsales comme une base de données pour exister avec certaines valeurs. Par exemple, vous pouvez souhaiter avoir un test qui vérifie que l'application s'affiche correctement avec plus de 100 clients dans la base de données, ou la page de commande fonctionne même si les noms de produits sont affichés en japonais.

Ces types de tests d'intégration dépendront de différents montages de test pour s'assurer qu'ils sont répétables et prévisibles.

Une bonne technique à utiliser consiste à stocker les données de test dans un dossier de votre dossier de test d'intégration appeléfixtures pour indiquer qu'il contient des données de test. Ensuite, dans vos tests, vous pouvez charger les données et exécuter le test.

Voici un exemple de cette structure si les données sont constituées de fichiers JSON:

project/
│
├── my_app/
│   └── __init__.py
│
└── tests/
    |
    └── unit/
    |   ├── __init__.py
    |   └── test_sum.py
    |
    └── integration/
        |
        ├── fixtures/
        |   ├── test_basic.json
        |   └── test_complex.json
        |
        ├── __init__.py
        └── test_integration.py

Dans votre scénario de test, vous pouvez utiliser la méthode.setUp() pour charger les données de test à partir d'un fichier de fixture dans un chemin connu et exécuter de nombreux tests par rapport à ces données de test. N'oubliez pas que vous pouvez avoir plusieurs cas de test dans un seul fichier Python, et la découverte deunittest exécutera les deux. Vous pouvez avoir un cas de test pour chaque ensemble de données de test:

import unittest


class TestBasic(unittest.TestCase):
    def setUp(self):
        # Load test data
        self.app = App(database='fixtures/test_basic.json')

    def test_customer_count(self):
        self.assertEqual(len(self.app.customers), 100)

    def test_existence_of_customer(self):
        customer = self.app.get_customer(id=10)
        self.assertEqual(customer.name, "Org XYZ")
        self.assertEqual(customer.address, "10 Red Road, Reading")


class TestComplexData(unittest.TestCase):
    def setUp(self):
        # load test data
        self.app = App(database='fixtures/test_complex.json')

    def test_customer_count(self):
        self.assertEqual(len(self.app.customers), 10000)

    def test_existence_of_customer(self):
        customer = self.app.get_customer(id=9999)
        self.assertEqual(customer.name, u"バナナ")
        self.assertEqual(customer.address, "10 Red Road, Akihabara, Tokyo")

if __name__ == '__main__':
    unittest.main()

Si votre application dépend de données provenant d'un emplacement distant, comme une API distante, vous voudrez vous assurer que vos tests sont reproductibles. Faire échouer vos tests car l'API est hors ligne ou il y a un problème de connectivité peut ralentir le développement. Dans ces types de situations, il est recommandé de stocker les appareils distants localement afin qu'ils puissent être rappelés et envoyés à l'application.

La bibliothèquerequests a un package complémentaire appeléresponses qui vous permet de créer des réponses fixes et de les enregistrer dans vos dossiers de test. En savoir plus suron their GitHub Page.

Test dans plusieurs environnements

Jusqu'à présent, vous avez testé sur une seule version de Python en utilisant un environnement virtuel avec un ensemble spécifique de dépendances. Vous voudrez peut-être vérifier que votre application fonctionne sur plusieurs versions de Python ou plusieurs versions d'un package. Tox est une application qui automatise les tests dans plusieurs environnements.

Installation de Tox

Tox est disponible sur PyPI en tant que package à installer viapip:

$ pip install tox

Maintenant que Tox est installé, il doit être configuré.

Configurer Tox pour vos dépendances

Tox est configuré via un fichier de configuration dans votre répertoire de projet. Le fichier de configuration Tox contient les éléments suivants:

  • La commande à exécuter pour exécuter des tests

  • Tous les packages supplémentaires requis avant l'exécution

  • Les versions cibles de Python à tester

Au lieu d'avoir à apprendre la syntaxe de configuration de Tox, vous pouvez prendre une longueur d'avance en exécutant l'application de démarrage rapide:

$ tox-quickstart

L'outil de configuration Tox vous posera ces questions et créera un fichier similaire au suivant danstox.ini:

[tox]
envlist = py27, py36

[testenv]
deps =

commands =
    python -m unittest discover

Avant de pouvoir exécuter Tox, vous devez disposer d'un fichiersetup.py dans votre dossier d'application contenant les étapes d'installation de votre package. Si vous n’en avez pas, vous pouvez suivrethis guide pour savoir comment créer unsetup.py avant de continuer.

Sinon, si votre projet n'est pas destiné à être distribué sur PyPI, vous pouvez ignorer cette exigence en ajoutant la ligne suivante dans le fichiertox.ini sous l'en-tête[tox]:

[tox]
envlist = py27, py36
skipsdist=True

Si vous ne créez pas desetup.py et que votre application a des dépendances de PyPI, vous devrez les spécifier sur un certain nombre de lignes dans la section[testenv]. Par exemple, Django aurait besoin des éléments suivants:

[testenv]
deps = django

Une fois cette étape terminée, vous êtes prêt à exécuter les tests.

Vous pouvez maintenant exécuter Tox, et cela créera deux environnements virtuels: un pour Python 2.7 et un pour Python 3.6. Le répertoire Tox est appelé.tox/. Dans le répertoire.tox/, Tox exécuterapython -m unittest discover sur chaque environnement virtuel.

Vous pouvez exécuter ce processus en appelant Tox sur la ligne de commande:

$ tox

Tox affichera les résultats de vos tests dans chaque environnement. La première fois qu'il s'exécute, Tox prend un peu de temps pour créer les environnements virtuels, mais une fois qu'il l'a fait, la deuxième exécution sera beaucoup plus rapide.

Exécution de Tox

La sortie de Tox est assez simple. Il crée un environnement pour chaque version, installe vos dépendances, puis exécute les commandes de test.

Il existe quelques options de ligne de commande supplémentaires dont il faut se souvenir.

Exécutez un seul environnement, tel que Python 3.6:

$ tox -e py36

Recréez les environnements virtuels, au cas où vos dépendances auraient changé ou quesite-packages serait corrompu:

$ tox -r

Exécutez Tox avec une sortie moins détaillée:

$ tox -q

Exécution de Tox avec une sortie plus détaillée:

$ tox -v

Pour plus d'informations sur Tox, consultez lesTox Documentation Website.

Automatiser l'exécution de vos tests

Jusqu'à présent, vous avez exécuté les tests manuellement en exécutant une commande. Il existe des outils pour exécuter des tests automatiquement lorsque vous apportez des modifications et les validez dans un référentiel de contrôle de source comme Git. Les outils de test automatisé sont souvent appelés outils CI / CD, qui signifie «intégration continue / déploiement continu». Ils peuvent exécuter vos tests, compiler et publier toutes les applications, et même les déployer en production.

Travis CI est l'un des nombreux services CI (intégration continue) disponibles.

Travis CI fonctionne parfaitement avec Python, et maintenant que vous avez créé tous ces tests, vous pouvez automatiser leur exécution dans le cloud! Travis CI est gratuit pour tous les projets open-source sur GitHub et GitLab et est disponible moyennant un supplément pour les projets privés.

Pour commencer, connectez-vous au site Web et authentifiez-vous avec vos informations d'identification GitHub ou GitLab. Créez ensuite un fichier appelé.travis.yml avec le contenu suivant:

language: python
python:
  - "2.7"
  - "3.7"
install:
  - pip install -r requirements.txt
script:
  - python -m unittest discover

Cette configuration demande à Travis CI de:

  1. Testez contre Python 2.7 et 3.7 (vous pouvez remplacer ces versions par celles que vous choisissez.)

  2. Installez tous les packages que vous répertoriez dansrequirements.txt (Vous devez supprimer cette section si vous n'avez aucune dépendance.)

  3. Exécutezpython -m unittest discover pour exécuter les tests

Une fois que vous avez validé et poussé ce fichier, Travis CI exécutera ces commandes chaque fois que vous pousserez vers votre référentiel Git distant. Vous pouvez consulter les résultats sur leur site Web.

Et après

Maintenant que vous avez appris à créer des tests, à les exécuter, à les inclure dans votre projet et même à les exécuter automatiquement, il existe quelques techniques avancées que vous pourriez trouver utiles à mesure que votre bibliothèque de tests se développe.

Introduction de linters à votre application

Tox et Travis CI ont une configuration pour une commande de test. La commande de test que vous avez utilisée tout au long de ce didacticiel estpython -m unittest discover.

Vous pouvez fournir une ou plusieurs commandes dans tous ces outils, et cette option est là pour vous permettre d'ajouter plus d'outils qui améliorent la qualité de votre application.

Un tel type d'application est appelé un linter. Un linter examinera votre code et le commentera. Il pourrait vous donner des conseils sur les erreurs que vous avez commises, corriger les espaces de fin et même prédire les bogues que vous pourriez avoir introduits.

Pour plus d'informations sur les linters, lisez lesPython Code Quality tutorial.

Linting passif avecflake8

Un linter populaire qui commente le style de votre code par rapport à la spécificationPEP 8 estflake8.

Vous pouvez installerflake8 en utilisantpip:

$ pip install flake8

Vous pouvez ensuite exécuterflake8 sur un seul fichier, un dossier ou un modèle:

$ flake8 test.py
test.py:6:1: E302 expected 2 blank lines, found 1
test.py:23:1: E305 expected 2 blank lines after class or function definition, found 1
test.py:24:20: W292 no newline at end of file

Vous verrez une liste d'erreurs et d'avertissements pour votre code queflake8 a trouvé.

flake8 est configurable sur la ligne de commande ou dans un fichier de configuration de votre projet. Si vous souhaitez ignorer certaines règles, comme lesE305 ci-dessus, vous pouvez les définir dans la configuration. flake8 inspectera un fichier.flake8 dans le dossier du projet ou un fichiersetup.cfg. Si vous avez décidé d'utiliser Tox, vous pouvez placer la section de configuration deflake8 danstox.ini.

Cet exemple ignore les répertoires.git et__pycache__ ainsi que la règleE305. En outre, il définit la longueur de ligne maximale à 90 au lieu de 80 caractères. Vous constaterez probablement que la contrainte par défaut de 79 caractères pour la largeur de ligne est très limitative pour les tests, car ils contiennent des noms de méthode longs, des littéraux de chaîne avec des valeurs de test et d'autres éléments de données qui peuvent être plus longs. Il est courant de définir la longueur de ligne pour les tests jusqu'à 120 caractères:

[flake8]
ignore = E305
exclude = .git,__pycache__
max-line-length = 90

Alternativement, vous pouvez fournir ces options sur la ligne de commande:

$ flake8 --ignore E305 --exclude .git,__pycache__ --max-line-length=90

Une liste complète des options de configuration est disponible sur lesDocumentation Website.

Vous pouvez maintenant ajouterflake8 à votre configuration CI. Pour Travis CI, cela ressemblerait à ceci:

matrix:
  include:
    - python: "2.7"
      script: "flake8"

Travis lira la configuration en.flake8 et échouera la construction si des erreurs de peluchage se produisent. Assurez-vous d'ajouter la dépendanceflake8 à votre fichierrequirements.txt.

Linting agressif avec un formateur de code

flake8 est un linter passif: il recommande des changements, mais il faut aller changer le code. Une approche plus agressive est un formateur de code. Les formateurs de code changeront automatiquement votre code pour répondre à une collection de pratiques de style et de mise en page.

black est un formateur très impitoyable. Il n'a pas d'options de configuration et il a un style très spécifique. Cela le rend idéal comme outil de dépôt pour mettre dans votre pipeline de test.

Note:black nécessite Python 3.6+.

Vous pouvez installerblack via pip:

$ pip install black

Ensuite, pour exécuterblack sur la ligne de commande, indiquez le fichier ou le répertoire que vous souhaitez formater:

$ black test.py

Garder votre code de test propre

Lors de l'écriture de tests, vous pouvez constater que vous finissez par copier et coller du code beaucoup plus que vous ne le feriez dans des applications normales. Les tests peuvent parfois être très répétitifs, mais ce n'est en aucun cas une raison pour laisser votre code bâclé et difficile à maintenir.

Au fil du temps, vous développerez beaucoup detechnical debt dans votre code de test, et si vous avez des changements importants dans votre application qui nécessitent des changements dans vos tests, cela peut être une tâche plus lourde que nécessaire en raison de la façon dont vous avez structuré leur.

Essayez de suivre le principe deDRY lors de l’écriture des tests:Don’tRepeatYourself.

Les appareils de test et les fonctions sont un excellent moyen de produire un code de test plus facile à maintenir. De plus, la lisibilité compte. Envisagez de déployer un outil de linting commeflake8 sur votre code de test:

$ flake8 --max-line-length=120 tests/

Test de dégradation des performances entre les modifications

Il existe de nombreuses façons de comparer le code en Python. La bibliothèque standard fournit le moduletimeit, qui peut chronométrer des fonctions plusieurs fois et vous donner la distribution. Cet exemple exécuteratest() 100 fois etprint() la sortie:

def test():
    # ... your code

if __name__ == '__main__':
    import timeit
    print(timeit.timeit("test()", setup="from __main__ import test", number=100))

Une autre option, si vous avez décidé d'utiliserpytest comme lanceur de test, est le pluginpytest-benchmark. Cela fournit un appareilpytest appelébenchmark. Vous pouvez passerbenchmark() à n'importe quel appelable, et il enregistrera la synchronisation de l'appelable dans les résultats depytest.

Vous pouvez installerpytest-benchmark depuis PyPI en utilisantpip:

$ pip install pytest-benchmark

Ensuite, vous pouvez ajouter un test qui utilise le luminaire et réussit l'appelable à exécuter:

def test_my_function(benchmark):
    result = benchmark(test)

L'exécution depytest vous donnera désormais des résultats de référence:

Pytest benchmark screenshot

Plus d'informations sont disponibles sur lesDocumentation Website.

Test des failles de sécurité dans votre application

Un autre test que vous voudrez exécuter sur votre application consiste à vérifier les erreurs de sécurité ou les vulnérabilités courantes.

Vous pouvez installerbandit depuis PyPI en utilisantpip:

$ pip install bandit

Vous pouvez ensuite passer le nom de votre module d'application avec l'indicateur-r, et cela vous donnera un résumé:

$ bandit -r my_sum
[main]  INFO    profile include tests: None
[main]  INFO    profile exclude tests: None
[main]  INFO    cli include tests: None
[main]  INFO    cli exclude tests: None
[main]  INFO    running on Python 3.5.2
Run started:2018-10-08 00:35:02.669550

Test results:
        No issues identified.

Code scanned:
        Total lines of code: 5
        Total lines skipped (#nosec): 0

Run metrics:
        Total issues (by severity):
                Undefined: 0.0
                Low: 0.0
                Medium: 0.0
                High: 0.0
        Total issues (by confidence):
                Undefined: 0.0
                Low: 0.0
                Medium: 0.0
                High: 0.0
Files skipped (0):

Comme pourflake8, les règles que les indicateursbandit sont configurables, et s'il y en a que vous souhaitez ignorer, vous pouvez ajouter la section suivante à votre fichiersetup.cfg avec les options:

[bandit]
exclude: /test
tests: B101,B102,B301

Plus de détails sont disponibles auGitHub Website.

Conclusion

Python a rendu les tests accessibles en intégrant les commandes et les bibliothèques dont vous avez besoin pour valider que vos applications fonctionnent comme prévu. Démarrer les tests en Python n'a pas besoin d'être compliqué: vous pouvez utiliserunittest et écrire de petites méthodes maintenables pour valider votre code.

Au fur et à mesure que vous en apprenez davantage sur les tests et que votre application se développe, vous pouvez envisager de passer à l'un des autres frameworks de test, commepytest, et commencer à tirer parti de fonctionnalités plus avancées.

Merci pour la lecture. J'espère que vous aurez un futur sans bug avec Python!