tutorial - python mock__init__




Puis-je patcher un décorateur Python avant qu'il n'emballe une fonction? (5)

J'ai une fonction avec un décorateur que j'essaye de tester avec l'aide de la bibliothèque Python Mock . Je voudrais utiliser mock.patch pour remplacer le décorateur réel avec un décorateur de contournement qui appelle simplement la fonction. Ce que je n'arrive pas à comprendre, c'est comment appliquer le patch avant que le vrai décorateur n'emballe la fonction. J'ai essayé quelques variations différentes sur la cible de patch et j'ai réorganisé le patch et les instructions d'import, mais sans succès. Des idées?


Concept

Cela peut sembler un peu étrange mais on peut patcher sys.path , avec une copie de lui-même, et effectuer une importation dans le cadre de la fonction de test. Le code suivant montre le concept.

from unittest.mock import patch
import sys

@patch('sys.modules', sys.modules.copy())
def testImport():
 oldkeys = set(sys.modules.keys())
 import MODULE
 newkeys = set(sys.modules.keys())
 print((newkeys)-(oldkeys))

oldkeys = set(sys.modules.keys())
testImport()                       -> ("MODULE") # Set contains MODULE
newkeys = set(sys.modules.keys())
print((newkeys)-(oldkeys))         -> set()      # An empty set

MODULE peut ensuite être remplacé par le module que vous testez. (Cela fonctionne en Python 3.6 avec MODULE substitué avec xml par exemple)

OP

Pour votre cas, disons que la fonction de décorateur réside dans le pretty module et que la fonction décorée réside dans le present , alors vous pretty.decorator patcher pretty.decorator utilisant la machinerie pretty.decorator et remplacer MODULE par present . Quelque chose comme ce qui suit devrait fonctionner (non testé).

class TestDecorator (unittest.TestCase): ...

  @patch(`pretty.decorator`, decorator)
  @patch(`sys.path`, sys.path.copy())
  def testFunction(self, decorator) :
   import present
   ...

Explication

Cela fonctionne en fournissant un sys.path "propre" pour chaque fonction de test, en utilisant une copie du sys.path courant du module de test. Cette copie est effectuée lors de la première analyse du module, ce qui garantit un sys.path cohérent pour tous les tests.

Nuances

Il y a cependant quelques implications. Si la structure de test exécute plusieurs modules de test sous la même session python, tout module de test qui importe MODULE globalement casse tout module de test qui l'importe localement. Cela oblige à effectuer l'importation localement partout. Si la structure exécute chaque module de test sous une session python séparée, cela devrait fonctionner. De même, vous ne pouvez pas importer globalement MODULE dans un module de test où vous importez MODULE localement.

Les importations locales doivent être effectuées pour chaque fonction de test dans une sous-classe de unittest.TestCase . Il est peut-être possible de l'appliquer à la sous-classe unittest.TestCase en faisant directement une importation particulière du module disponible pour toutes les fonctions de test dans la classe.

Intégré

Ceux qui se builtin avec les imports builtin trouveront que remplacer MODULE par sys , os etc. échouera, puisque ceux-ci sont déjà lus sur sys.path lorsque vous essayez de le copier. Le truc ici est d'invoquer Python avec les imports intégrés désactivés, je pense que python -X test.py fera mais j'oublie le drapeau approprié (Voir python --help ). Ceux-ci peuvent ensuite être importés localement en utilisant import builtins , IIRC.


Ce qui suit a fonctionné pour moi:

  1. Éliminer l'instruction d'importation qui charge la cible de test.
  2. Corrigez le décorateur au démarrage du test tel qu'appliqué ci-dessus.
  3. Appelez importlib.import_module () immédiatement après le patch pour charger la cible de test.
  4. Exécuter des tests normalement.

Ça a marché comme sur des roulettes.


Les décorateurs sont appliqués au moment de la définition de la fonction. Pour la plupart des fonctions, c'est quand le module est chargé. (Les fonctions qui sont définies dans d'autres fonctions ont le décorateur appliqué chaque fois que la fonction englobante est appelée.)

Donc, si vous voulez personnaliser un décorateur, ce que vous devez faire est:

  1. Importer le module qui le contient
  2. Définir la fonction de décorateur simulé
  3. Par exemple, module.decorator = mymockdecorator
  4. Importez le (s) module (s) utilisant le décorateur ou utilisez-le dans votre propre module

Si le module qui contient le décorateur contient également des fonctions qui l'utilisent, celles-ci sont déjà décorées au moment où vous pouvez les voir, et vous êtes probablement SOL

Éditer pour refléter les changements de Python depuis que j'ai écrit ceci: Si le décorateur utilise functools.wraps() et que la version de Python est assez récente, vous pouvez extraire la fonction originale en utilisant l' __wrapped__ __wrapped__ et la re-décorer, mais ce n'est pas garanti, et le décorateur que vous voulez remplacer n'est peut-être pas le seul décorateur appliqué.


Peut-être que vous pouvez appliquer un autre décorateur sur les définitions de tous vos décorateurs qui vérifie fondamentalement une variable de configuration pour voir si le mode de test est destiné à être utilisé.
Si oui, il remplace le décorateur qu'il décore par un décorateur factice qui ne fait rien.
Sinon, il laisse passer ce décorateur.


pour @lru_cache (max_size = 1000)


class MockedLruCache(object):
def __init__(self, maxsize=0, timeout=0):
    pass

def __call__(self, func):
    return func

cache.LruCache = MockedLruCache

Si vous utilisez un décorateur qui n'a pas de paramètres, vous devez:

def MockAuthenticated(func):
    return func

from tornado import web web.authenticated = MockAuthenticated





monkeypatching