c’est juste un fait de la vie, à mesure que le code grandit, vous devrez commencer à ajouter des moqueurs à votre suite de tests. Ce qui a commencé comme un joli petit projet de deux classes parle maintenant à des services externes et vous ne pouvez plus le tester confortablement.

C’est pourquoi Python est livré avecunittest.mock, une partie puissante de la bibliothèque standard pour les dépendances de stubbing et les effets secondaires moqueurs.

Cependant, unittest.mock n’est pas particulièrement intuitif.,

je me suis souvent demandé Pourquoi ma recette de référence ne fonctionnait pas pour un cas particulier, alors j’ai mis en place cette feuille de triche pour m’aider et aider les autres à se moquer rapidement.

Vous pouvez trouver les exemples de code dans le dépôt Github de l’article. J’utiliserai Python 3.6, si vous utilisez 3.2 ou moins, vous devrez utiliser le package mock PyPI.,

Les exemples sont écrits en utilisant les classes unittest.TestCasepour simplifier leur exécution sans dépendances, mais vous pouvez les écrire en tant que fonctions en utilisant pytestpresque directement,unittest.mock fonctionnera très bien. Si vous êtes un utilisateurpytest, je vous encourage à jeter un œil à l’excellente bibliothèquepytest-mock.

le point central du module unittest.mockest, bien sûr, la classe Mock., La caractéristique principale d’un Mock objet est qu’il sera de retour un autre Mock exemple lorsque:

  • accès à l’un de ses attributs
  • l’appel de l’objet lui-même

C’est le comportement par défaut, mais il peut être remplacé dans différentes manières. Par exemple, vous pouvez attribuer une valeur à un attribut dans le Mock par:

  • attribuez-le directement, comme vous le feriez avec n’importe quel objet Python.,
  • utilisez la méthodeconfigure_mock sur une instance.
  • ou passez des arguments de mots clés à la classeMock lors de la création.

Pour remplacer les appels à la maquette que vous aurez besoin de configurer son return_value propriété, également disponible comme un argument mot-clé dans la balise Mock initialiseur., Le Mock retournera toujours la même valeur sur tous les appels, ceci, encore une fois, peut également être configuré en utilisant l’attribut side_effect:

  • Si vous souhaitez renvoyer des valeurs différentes à chaque appel, vous pouvez affecter un itérable à side_effect.
  • Si vous souhaitez déclencher une exception lors de l’appel de la maquette, vous pouvez simplement affecter l’objet exception à side_effect.,

Avec tous ces outils, nous pouvons maintenant créer des talons pour n’importe quel objet Python, qui sera parfait pour les entrées de notre système. Mais quid des résultats?

Si vous créez un programme CLI pour télécharger tout L’Internet, vous ne voulez probablement pas télécharger tout L’Internet à chaque test. Au lieu de cela, il suffirait d’affirmer que requests.download_internet (pas une méthode réelle) a été appelée de manière appropriée. Mock vous donne des méthodes pratiques pour le faire.,

Remarque: dans notre exemple: assert_called_once a échoué, cela montre un autre aspect clé de la Mock objets, ils enregistrent toutes les interactions avec eux et vous pouvez ensuite vérifier ces interactions.

Par exemple, vous pouvez utiliser call_count pour récupérer le nombre d’appels à la balise Mock, et utiliser call_args ou call_args_list pour inspecter les arguments de la dernière ou de tous les appels respectivement.,

Si cela n’est pas pratique à tout moment, vous pouvez utiliser la méthodereset_mock pour effacer les interactions enregistrées, notez que la configuration ne sera pas réinitialisée, juste les interactions.

Enfin, permettez-moi de vous présenter MagicMock, une sous-classe de: Mock qui implémente par défaut de la magie ou dsous méthodes. Cela rend MagicMock idéal pour simuler le comportement de classe, c’est pourquoi c’est la classe par défaut lors de la correction.,

Nous sommes maintenant prêts à commencer à nous moquer et à isoler l’unité en cours de test. Voici quelques recettes à garder à l’esprit:

Patch on import

la principale façon d’utiliser unittest.mock est de patcher les importations dans le module testé en utilisant la fonction patch.

patch intercepteraimport les instructions identifiées par une chaîne (plus à ce sujet plus tard), et retournera une instanceMock que vous pouvez préconfigurer en utilisant les techniques que nous avons discutées ci-dessus.,

Imaginons que nous voulons tester cette fonction très simple:

Notez que nous sommes importateur os et l’appel de getcwd pour obtenir le répertoire de travail courant. Nous ne voulons pas vraiment l’appeler sur nos tests, car ce n’est pas important pour notre code et la valeur de retour peut différer entre les environnements sur lesquels il s’exécute.

comme mentionné ci-dessus, nous devons fournirpatch avec une chaîne représentant notre importation spécifique., Nous ne voulons pas fournir simplement os.getcwd car cela le patcherait pour tous les modules, nous voulons plutôt fournir le module testé import de os , c’est-à-dire work.os . Lorsque le module est importé patch fonctionne sa magie et renvoie un Mock à la place.,

Sinon, on peut utiliser le décorateur version de patch , remarque cette fois que le test a un paramètre supplémentaire: mocked_os pour qui la balise Mock est injecté dans le test.,

patch va de l’avant les arguments mots-clefs de la balise Mock de classe, afin de configurer un return_value nous avons simplement l’ajouter en tant que:

les Moqueries des classes

C’est assez courant de patch classes complètement ou partiellement., Helper par "db"

  • Worker renvoie le chemin prévu fourni par Helper
  • afin de tester Worker dans un isolement complet, nous avons besoin de patcher l’ensemble de l’ Helper catégorie:

    Notez le double return_value dans l’exemple, simplement à l’aide de MockHelper.get_path.return_value ne fonctionnait plus depuis dans le code que nous appelons get_path sur une instance de la classe elle-même.,

    la syntaxe de chaînage est légèrement déroutante mais n’oubliez pas queMagicMock renvoie un autreMagicMock lors des appels__init__. Ici, nous configurons toutes les fausses instancesHelper créées parMockHelper pour renvoyer ce que nous attendons des appels àget_path qui est la seule méthode qui nous intéresse dans notre test.,

    spécification de classe

    une conséquence de la flexibilité deMock est qu’une fois que nous nous sommes moqués d’une classe, Python ne lèvera pasAttributeError car il renverra simplement de nouvelles instances deMagicMock pour fondamentalement tout. C’est généralement une bonne chose, mais cela peut entraîner un comportement déroutant et potentiellement des bugs. Par exemple, l’écriture de l’essai suivant,

    en silence à passer avec aucun avertissement totalement absent de la faute de frappe dans la assrt_called_once .,

    en Outre, si nous étions à renommer Helper.get_path de Helper.get_folder, mais oublier de mettre à jour l’appel dans Worker nos tests va toujours passer:

    Heureusement Mock est livré avec un outil pour éviter ces erreurs, speccing.

    en termes simples, il préconfigure les mocks pour ne répondre qu’aux méthodes qui existent réellement dans la classe spec., Il existe plusieurs façons de définir les spécifications, mais le plus simple est de simplement passer autospec=True à l’appel patch, qui configurera le Mock pour se comporter comme l’objet moqué, soulevant des exceptions pour les attributs manquants et les signatures incorrectes selon les besoins., Par exemple:

    classe Partielle moqueur

    Si vous êtes moins enclin à tester dans un isolement complet, vous pouvez également partiellement patch une classe à l’aide de patch.object:

    Ici patch.object permet de configurer un moqué version de get_path seulement, en laissant le reste du comportement intacte., Bien sûr, cela signifie que le test n’est plus ce que vous considéreriez strictement comme un test unitaire, mais vous pouvez être d’accord avec cela.

    se moquant des fonctions intégrées et des variables d’environnement

    dans les exemples précédents, nous avons négligé de tester un aspect particulier de notre classe simple, l’appelprint. Si dans le contexte de votre application, cela est important, comme une commande CLI par exemple, nous devons faire des assertions contre cet appel.,

    print est, bien sûr, une fonction intégrée en Python, ce qui signifie que nous n’avons pas besoin de l’importer dans notre module, ce qui va à l’encontre de ce que nous avons discuté ci-dessus à propos du correctif à l’importation., Imaginez quand même que nous avions cette version un peu plus compliquée de notre fonction:

    Nous Pouvons Écrire un test comme suit:

    notez quelques choses:

    1. nous pouvons nous moquer deprint sans problème et affirmer qu’il y a eu un appel en suivant la règle « patch on import”. Il s’agissait cependant d’un changement introduit dans 3.,5, auparavant, il fallait ajouter la balise create=True à la balise patch appel à signal unittest.mockpour créer un Mock même si aucune importation correspond à l’identificateur.
    2. nous utilisons patch.dict pour injecter une variable d’environnement temporaire dans os.environ, ceci est extensible à tout autre dictionnaire que nous aimerions moquer.,
    3. nous imbriquons plusieurs appels du gestionnaire de contexte patch mais en utilisant uniquement as dans le premier car c’est celui que nous devons utiliser pour appeler assert_called_once_with .,

    Si vous n’aimez pas les gestionnaires de contexte d’imbrication, vous pouvez également écrire les appels patch sous la forme décorateur:

    remarque cependant, l’ordre des arguments au test correspond à l’ordre D’empilement des décorateurs, et aussi quepatch.dictn’injecte pas d’argument.,ce scénario très commun:

    Vous pouvez, bien sûr, ajouter un fichier de montage réel, mais dans les cas réels, ce n’est peut-être pas une option, nous pouvons plutôt nous moquer de la sortie du gestionnaire de contexte pour d76c511e0a »>

    objet:

    Il n’y a rien de spécial ici sauf la méthode magique__enter__, nous devons juste nous rappeler la mécanique sous-jacente des gestionnaires de contexte et une utilisation intelligente de notre fidèle MagicMock.,

    attributs de classe moqueurs

    Il existe de nombreuses façons d’y parvenir, mais certaines sont plus infaillibles que d’autres. Dites que vous avez écrit le code suivant:

    Vous pouvez tester le code, sans se moque de deux façons:

    1. Si le code en cours de test accède à l’attribut à l’aide de self.ATTRIBUTE, ce qui est le cas dans cet exemple, vous pouvez simplement définir l’attribut directement dans l’instance. C’est assez sûr car le changement est limité à cette seule instance.,
    2. Sinon, vous pouvez également définir l’attribut dans la classe importée dans le test avant de créer une instance. Cela modifie cependant l’attribut de classe que vous avez importé dans votre test, ce qui pourrait affecter les tests suivants, vous devez donc vous souvenir de le réinitialiser.

    le principal inconvénient de cette approche non simulée est que si vous renommez l’attribut à tout moment, vos tests échoueront et l’erreur ne signalera pas directement cette non-concordance de nommage.,

    pour résoudre cela, nous pouvons utiliser patch.object sur la classe importée qui se plaindra si la classe n’a pas l’attribut spécifié.

    Voici les quelques tests d’utilisation de chaque technique:

    les Moqueries de classe aides

    L’exemple suivant est la racine de nombreux problèmes concernant monkeypatching à l’aide de Maquette. Il apparaît généralement sur des bases de code plus matures qui commencent à utiliser des frameworks et des helpers lors de la définition de classe., Par exemple, imaginez cet hypothétique Field assistant de classe:

    son but est d’envelopper et d’améliorer un attribut dans une autre classe, un modèle assez que vous pourriez généralement voir dans ORM ou bibliothèques. Ne vous préoccupez pas trop des détails, notez simplement qu’il y a une valeur type Et default transmise.,

    Maintenant, prenez cette autre échantillon de la production de code:

    Cette classe permet l’utilisation de la balise Field de classe par la définition d’ country attribut comme étant celui dont le type est str et un default le premier élément de la balise COUNTRIES constante. La logique à l’essai est que, selon le pays, une réduction est appliquée.,

    Pour lequel on peut écrire le test suivant:

    Mais ce ne serait PAS passer.,

    parcourons le test:

    1. d’abord, il corrige les pays par défaut dans pricer pour être une liste avec une seule entrée GB,
    2. cela devrait rendre l’attribut CountryPricer.country par défaut à la définition inclut default=COUNTRIES,
    3. il instancie alors le CountryPricer et demande le prix réduit pour GB !

    alors, que se passe-t-il?,

    la cause profonde de ceci réside dans le comportement de Python lors de l’importation, mieux décrit dans L’excellent Python Fluent de Luciano Ramalho au chapitre 21:

    pour les classes, L’histoire est différente: au moment de l’importation, l’interpréteur exécute le corps de chaque classe, même le corps des classes imbriquées L’exécution d’un corps de classe signifie que les attributs et les méthodes de la classe sont définis, puis que l’objet de classe lui-même est construit.,

    en appliquant ceci à notre exemple:

    1. l’instance country de l’attribut Field est construite avant que le test ne soit exécuté au moment de l’importation,
    2. Python lit le corps de la classe,= »0cf935f142″>

    qui est défini à ce stade à l’instanceField,

  • notre code de test s’exécute mais il est trop tard pour nous Patcher COUNTRIES et obtenir une assertion appropriée.,
  • à Partir de la description ci-dessus, vous pouvez essayer de retarder l’importation du module jusqu’à l’intérieur de l’tests, quelque chose comme:

    Ceci, cependant, va quand même pas se passer comme mock.patch importer le module, puis monkeypatch, entraînant dans la même situation qu’avant.,

    Pour contourner cela, nous avons besoin d’embrasser l’état de la balise CountryPricer de classe au moment de l’essai et patch default le déjà initialisé Field exemple:

    Cette solution n’est pas idéale car il nécessite la connaissance de l’intérieur de la balise Field, qui ne peut pas avoir ou à utiliser si vous utilisez une bibliothèque externe, mais il fonctionne sur cette vague, certes simplifié cas.,

    Ce problème de temps d’importation est l’un des principaux problèmes rencontrés lors de l’utilisation deunittest.mock. Vous devez vous rappeler en l’utilisant qu’au moment de l’importation, le code au niveau supérieur des modules est exécuté, y compris les corps de classe.

    Si la logique que vous testez dépend de l’une de ces logiques, vous devrez peut-être repenser la façon dont vous utilisez patch en conséquence.,

    conclusion

    Cette introduction et cette feuille de triche devraient vous moquer avecunittest.mock, mais je vous encourage à lire la documentation à fond, il y a beaucoup d’astuces plus intéressantes à apprendre.

    Il convient de mentionner qu’il existe des alternatives àunittest.mock, en particulier la bibliothèquepretendD’Alex Gaynor en combinaison avec le luminairepytest. Comme Alex le souligne, en utilisant ces deux bibliothèques, vous pouvez adopter une approche différente pour rendre les moqueries plus strictes et plus prévisibles., Certainement une approche qui mérite d « être explorée mais en dehors du champ d » application de cet article.

    unittest.mock est actuellement le standard pour se moquer en Python et vous le trouverez dans pratiquement toutes les bases de code. Avoir une bonne compréhension de cela vous aidera à écrire de meilleurs tests plus rapidement.

    Articles

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *