python unit-testing mocking doctest zope.component

Mocking ImportError en Python



unit-testing doctest (3)

Intento esto por casi dos horas, sin suerte.

Tengo un módulo que se ve así:

try: from zope.component import queryUtility # and things like this except ImportError: # do some fallback operations <-- how to test this?

Más adelante en el código:

try: queryUtility(foo) except NameError: # do some fallback actions <-- this one is easy with mocking # zope.component.queryUtility to raise a NameError

¿Algunas ideas?

EDITAR:

La sugerencia de Alex no parece funcionar:

>>> import __builtin__ >>> realimport = __builtin__.__import__ >>> def fakeimport(name, *args, **kw): ... if name == ''zope.component'': ... raise ImportError ... realimport(name, *args, **kw) ... >>> __builtin__.__import__ = fakeimport

Al ejecutar las pruebas:

aatiis@aiur ~/work/ao.shorturl $ ./bin/test --coverage . Running zope.testing.testrunner.layer.UnitTests tests: Set up zope.testing.testrunner.layer.UnitTests in 0.000 seconds. Error in test /home/aatiis/work/ao.shorturl/src/ao/shorturl/shorturl.txt Traceback (most recent call last): File "/usr/lib64/python2.5/unittest.py", line 260, in run testMethod() File "/usr/lib64/python2.5/doctest.py", line 2123, in runTest test, out=new.write, clear_globs=False) File "/usr/lib64/python2.5/doctest.py", line 1361, in run return self.__run(test, compileflags, out) File "/usr/lib64/python2.5/doctest.py", line 1282, in __run exc_info) File "/usr/lib64/python2.5/doctest.py", line 1148, in report_unexpected_exception ''Exception raised:/n'' + _indent(_exception_traceback(exc_info))) File "/usr/lib64/python2.5/doctest.py", line 1163, in _failure_header out.append(_indent(source)) File "/usr/lib64/python2.5/doctest.py", line 224, in _indent return re.sub(''(?m)^(?!$)'', indent*'' '', s) File "/usr/lib64/python2.5/re.py", line 150, in sub return _compile(pattern, 0).sub(repl, string, count) File "/usr/lib64/python2.5/re.py", line 239, in _compile p = sre_compile.compile(pattern, flags) File "/usr/lib64/python2.5/sre_compile.py", line 507, in compile p = sre_parse.parse(p, flags) AttributeError: ''NoneType'' object has no attribute ''parse'' Error in test BaseShortUrlHandler (ao.shorturl) Traceback (most recent call last): File "/usr/lib64/python2.5/unittest.py", line 260, in run testMethod() File "/usr/lib64/python2.5/doctest.py", line 2123, in runTest test, out=new.write, clear_globs=False) File "/usr/lib64/python2.5/doctest.py", line 1351, in run self.debugger = _OutputRedirectingPdb(save_stdout) File "/usr/lib64/python2.5/doctest.py", line 324, in __init__ pdb.Pdb.__init__(self, stdout=out) File "/usr/lib64/python2.5/pdb.py", line 57, in __init__ cmd.Cmd.__init__(self, completekey, stdin, stdout) File "/usr/lib64/python2.5/cmd.py", line 90, in __init__ import sys File "<doctest shorturl.txt[10]>", line 4, in fakeimport NameError: global name ''realimport'' is not defined

Sin embargo, funciona cuando ejecuto el mismo código desde la consola interactiva de python.

MÁS EDITAR:

Estoy usando zope.testing y un archivo de prueba, shorturl.txt que tiene todas las pruebas específicas de esta parte de mi módulo. Primero estoy importando el módulo con zope.component disponible, para demostrar y probar el uso habitual. La ausencia de paquetes zope.* Se considera un caso extremo, así que lo estoy probando más tarde. Por lo tanto, tengo que reload() a reload() mi módulo, después de hacer zope.* disponible, de alguna manera.

Hasta ahora, incluso he intentado usar tempfile.mktempdir() y vaciar los zope/__init__.py y zope/component/__init__.py en el tempdir, luego inserto tempdir en sys.path[0] y sys.path[0] el viejo zope.* paquetes de sys.modules .

Tampoco funcionó.

AÚN MÁS EDITAR:

Mientras tanto, he intentado esto:

>>> class NoZope(object): ... def find_module(self, fullname, path): ... if fullname.startswith(''zope''): ... raise ImportError ... >>> import sys >>> sys.path.insert(0, NoZope())

Y funciona bien para el espacio de nombres del conjunto de pruebas (= para todas las importaciones en shorturl.txt ), pero no se ejecuta en mi módulo principal, ao.shorturl . Ni siquiera cuando lo reload() . ¿Alguna idea de por qué?

>>> import zope # ok, this raises an ImportError >>> reload(ao.shorturl) <module ...>

La importación de zope.interfaces genera un ImportError , por lo que no llega a la parte donde importo zope.component , y permanece en el espacio de nombres ao.shorturl . ¡¿Por qué?!

>>> ao.shorturl.zope.component # why?! <module ...>


Esto es lo que acerté en mis pruebas de unidad.

Utiliza PEP-302 "New Import Hooks" . (Advertencia: el documento PEP-302 y las notas de la versión más concisas que he vinculado no son exactamente exactas ).

Utilizo meta_path porque es lo más temprano posible en la secuencia de importación.

Si el módulo ya ha sido importado (como en mi caso, debido a que los unittest anteriores se burlan de él), entonces es necesario eliminarlo de sys.modules antes de reload a reload en el módulo dependiente.

Ensure we fallback to using ~/.pif if XDG doesn''t exist. >>> import sys >>> class _(): ... def __init__(self, modules): ... self.modules = modules ... ... def find_module(self, fullname, path=None): ... if fullname in self.modules: ... raise ImportError(''Debug import failure for %s'' % fullname) >>> fail_loader = _([''xdg.BaseDirectory'']) >>> sys.meta_path.append(fail_loader) >>> del sys.modules[''xdg.BaseDirectory''] >>> reload(pif.index) #doctest: +ELLIPSIS <module ''pif.index'' from ''...''> >>> pif.index.CONFIG_DIR == os.path.expanduser(''~/.pif'') True >>> sys.meta_path.remove(fail_loader)

Donde el código dentro de pif.index se ve así:

try: import xdg.BaseDirectory CONFIG_DIR = os.path.join(xdg.BaseDirectory.xdg_data_home, ''pif'') except ImportError: CONFIG_DIR = os.path.expanduser(''~/.pif'')

Para responder la pregunta sobre por qué el módulo recién recargado tiene propiedades de cargas antiguas y nuevas, aquí hay dos archivos de ejemplo.

El primero es un módulo y con un caso de falla de importación.

# y.py try: import sys _loaded_with = ''sys'' except ImportError: import os _loaded_with = ''os''

El segundo es x que demuestra cómo dejar identificadores para un módulo puede afectar sus propiedades cuando se recarga.

# x.py import sys import y assert y._loaded_with == ''sys'' assert y.sys class _(): def __init__(self, modules): self.modules = modules def find_module(self, fullname, path=None): if fullname in self.modules: raise ImportError(''Debug import failure for %s'' % fullname) # Importing sys will not raise an ImportError. fail_loader = _([''sys'']) sys.meta_path.append(fail_loader) # Demonstrate that reloading doesn''t work if the module is already in the # cache. reload(y) assert y._loaded_with == ''sys'' assert y.sys # Now we remove sys from the modules cache, and try again. del sys.modules[''sys''] reload(y) assert y._loaded_with == ''os'' assert y.sys assert y.os # Now we remove the handles to the old y so it can get garbage-collected. del sys.modules[''y''] del y import y assert y._loaded_with == ''os'' try: assert y.sys except AttributeError: pass assert y.os


Si no le importa cambiar su programa, también puede colocar la llamada de importación en una función y aplicarle un parche en sus pruebas.


Solo revise en el builtins su propia versión de __import__ - puede generar lo que desee cuando reconozca que se está solicitando en los módulos específicos para los que desea simular errores. Consulte los documentos para obtener información detallada. Aproximadamente:

try: import builtins except ImportError: import __builtin__ as builtins realimport = builtins.__import__ def myimport(name, globals, locals, fromlist, level): if ...: raise ImportError return realimport(name, globals, locals, fromlist, level) builtins.__import__ = myimport

En lugar de ... , puede hardcode name == ''zope.component'' , o arregle las cosas de manera más flexible con una devolución de llamada propia que puede aumentar las importaciones bajo demanda en diferentes casos, dependiendo de sus necesidades específicas de prueba, sin requiriendo que codifiques múltiples funciones __import__ -alike ;-).

Tenga en cuenta también que si lo que usa, en lugar de import zope.component o from zope.component import something , es from zope import component , el name será ''zope'' , y ''component'' será entonces el único elemento en la lista fromlist .

Editar : los documentos para la función __import__ dicen que el nombre para importar está builtin (como en Python 3), pero de hecho necesita __builtins__ - He editado el código anterior para que funcione de cualquier manera.