用Python模拟恐怖

用Python模拟恐怖,python,unit-testing,mocking,doctest,zope.component,Python,Unit Testing,Mocking,Doctest,Zope.component,我已经试了差不多两个小时了,运气不好 我有一个模块,如下所示: try: from zope.component import queryUtility # and things like this except ImportError: # do some fallback operations <-- how to test this? 运行测试时: aatiis@aiur ~/work/ao.shorturl $ ./bin/test --coverage . R

我已经试了差不多两个小时了,运气不好

我有一个模块,如下所示:

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

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
它对测试套件的名称空间(即
shorturl.txt
中的所有导入)运行良好,但它没有在我的主模块
ao.shorturl
中执行。即使在我重新加载()时也不行。知道为什么吗

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

只需将monkeypatch插入
内置程序
您自己版本的
\uuuuu import\uuuuuu
——当它识别出在您想要模拟错误的特定模块上调用它时,它可以引发您想要的任何错误。有关详细信息,请参见。大致:

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
代替
,您可以硬编码
名称=='zope.component'
,或者通过自己的回调更灵活地安排事情,使导入在不同情况下根据您的特定测试需要而按需增加,而无需编写多个类似的
\uuuuuuuu导入\uuuuuuuu
-函数;-)

还要注意的是,如果您使用的不是从zope.component导入的
或从zope.component导入的
,而是从zope导入的
,那么
名称将是
'zope'
,并且
'component'
将是
from列表中的唯一项


编辑:用于
\uuuu导入
函数的文档说要导入的名称是
内置的
(如Python 3),但事实上您需要
\uu内置的
——我已经编辑了上面的代码,以便它可以以任何方式工作。

这就是我在单元测试中调整的

它使用。(警告:我链接的PEP-302文档和更简洁的发行说明并不准确。)

我使用
meta_路径
,因为它在导入序列中尽可能早

如果模块已经被导入(就像我的情况一样,因为早期的unittests针对它进行模拟),那么在对依赖模块执行
重新加载之前,有必要将其从sys.modules中删除

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)

要回答为什么新重新加载的模块具有旧加载和新加载的属性的问题,这里有两个示例文件

第一个是模块
y
,带有导入失败案例

# y.py

try:
    import sys

    _loaded_with = 'sys'
except ImportError:
    import os

    _loaded_with = 'os'
第二个是
x
,它演示了在重新加载时为模块保留句柄如何影响其属性

# 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

如果您不介意更改程序本身,也可以将导入调用放入函数中,并在测试中对其进行修补。

啊,谢谢!出于某种原因,我尝试执行
def\uu import\uu()
,但没有将其分配给
内置;我真傻。有趣的是,我正在这里读你的答案:-你认为如果我不将queryUtility导入到我的模块的作用域中,情况会变得更容易吗?@Attila,如果你从zope import component
导入
,然后使用
component.queryUtility
,它会让你更容易,例如,在某些时候使用真实的东西,还有其他时候的仿制品。正如我在回答中所写的那样,我确实建议将其作为一个一般性的东西,这是我们在Google编写Python代码的方式的一部分(当然,有时可以使用
As
子句来缩短导入的名称,但这不会改变语义),您的
\uuuu import\uuuu
-like函数将
'zope'
视为
name
参数,将
'component'
视为
fromlist
参数中的一项(唯一一个,除非您从zope导入this、that、component
等;-);因此,请确保相应地触发。感谢您的建议,我将采用这种编码方式,因为它似乎在这种情况下很有帮助。然而,我刚刚意识到你的答案对我不起作用(如果我用
\uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuiuuuuuuuuuuuuuuuuuiuuuuuuuuuuuuuuuuuuuuuiuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu2.5替换
的话,它在交互式控制台上确实起作用,不<。再次编辑以修复。太好了,现在我设法提出了一个
ImportError
,这就是我所需要的。有趣的是:如果我重新加载
ao.shorturl
,并且在其中我有
try:import zope.component,zope.interface;除了ImportError:fallback()
,我得到了
zope.component
的第一个
ImportError
,zope.interface仍将在ao.shorturl(ao.shorturl.zope.interface)中可用。为什么会这样?我刚刚又增加了一节来描述为什么会这样。tl;dr,您需要在重新加载
之前
删除ao.shorturl
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
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)
try:
    import xdg.BaseDirectory

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

try:
    import sys

    _loaded_with = 'sys'
except ImportError:
    import os

    _loaded_with = 'os'
# 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