在导入模块之前设置nose-doctest模块的夹具

5 投票
2 回答
523 浏览
提问于 2025-04-28 06:43

我使用nose来收集测试,同时也想用它的doctest插件。我有一个模块需要一个固定的环境才能被导入。因此,我不能使用nose的模块固定环境,因为它们是从正在测试的模块中加载的。有没有办法在模块外部为nose-doctest指定模块固定环境呢?

在某些情况下,一个选择是检测是否在doctest下运行,并在模块开始时应用固定环境。我也很想听听这个用例的答案。

不过,有些情况是这样行不通的:当导入失败时,比如出现SyntaxError,模块代码根本不会运行。在我的情况下,我主要开发的代码需要同时兼容python 2和python 3(不使用2to3)。不过,有一些特定于python 3的模块,在python 2下运行时根本不应该被nose检查。那么我该怎么做呢?

编辑:最小可重现示例(针对SyntaxError的情况)

我有一个包含许多小模块的包,其中一些使用python 3的语法。包的结构如下:

~/pckg/
  __init__.py
  py3only.py
  ... (other modules)
  tests/
    test_py3only.py

一些测试是用unittest.TestCase写的,但我也想测试文档字符串中的代码示例。~/pckg/__init__.py是空的。

~/pckg/py3only.py:

def fancy_py3_func(a:"A function argument annotation (python 3 only syntax)"):
    """ A function using fancy syntax doubling it's input.

    >>> fancy_py3_func(4)
    8
    """
    return a*2

~/pckg/tests/test_py3only.py:

import sys, unittest

def setup_module():
    if sys.version_info[0] < 3:
        raise unittest.SkipTest("py3only unavailable on python "+sys.version)

class TestFancyFunc(unittest.TestCase):
    def test_bruteforce(self):
        from pckg.py3only import fancy_py3_func
        for k in range(10):
            self.assertEqual(fancy_py3_func(k),2*k)

在python 3上测试时,一切都能通过测试(从包含的文件夹运行,比如~):

~ nosetests3 -v --with-doctest pckg
Doctest: pckg.py3only.fancy_py3_func ... ok
test_bruteforce (test_py3only.TestFancyFunc) ... ok

在python 2上,~/pckg/tests/test_py2only.py的模块固定环境正确地检测到了情况并跳过了测试。然而,我们从~/pckg/py3only.py得到了一个SyntaxError

~ nosetests -v --with-doctest pckg 
Failure: SyntaxError (invalid syntax (py3only.py, line 1)) ... ERROR
SKIP: py3only unavailable on python 2.7.6 (default, Mar 22 2014, 22:59:56)

如果我能让nose在它的doctest插件尝试导入模块之前运行类似于~/pckg/tests/test_py3only.py:setup_module()的函数,就能解决这个问题。

看起来我最好的选择是写一个合适的顶层测试脚本来处理测试的收集……

暂无标签

2 个回答

1

Nose有以下选项:

  --doctest-fixtures=SUFFIX
                    Find fixtures for a doctest file in module with this
                    name appended to the base name of the doctest file

也许你可以把测试用例放到一个单独的文件里?

2

你可以使用 nose-exclude 这个插件来排除特定的测试文件、目录、类或方法。它提供了 --exclude-* 的选项。

如果有模块缺失,你需要用 mock 来修补 sys.modules

比如说,mycalc 模块里有一个 Calc 类,但我无法访问它,因为它缺失了。而且还有两个模块,mysuper_calcmysuper_calc3,后者是专门为 Python 3 设计的。这两个模块会导入 mycalc,而 mysuper_calc3 在 Python 2 下不应该被测试。那我该如何在一个纯文本文件中对它们进行文档测试呢?我猜这就是提问者的情况。

calc/mysuper_calc3.py

from sys import version_info
if version_info[0] != 3:
    raise Exception('Python 3 required')
from mycalc import Calc
class SuperCalc(Calc):
    '''This class implements an enhanced calculator
    '''
    def __init__(self):
        Calc.__init__(self)

    def add(self, n, m):
        return Calc.add(self, n, m)

calc/mysuper_calc.py

from mycalc import Calc

class SuperCalc(Calc):
    '''This class implements an enhanced calculator
    '''
    def __init__(self):
        Calc.__init__(self)

    def add(self, n, m):
        return Calc.add(self, n, m)

现在要模拟 mycalc

>>> from mock import Mock, patch
>>> mock = Mock(name='mycalc')

模块 mycalc 里有一个 Calc 类,它有一个 add 方法。我用 2+3 来测试 SuperCalc 实例的 add 方法。

>>> mock.Calc.add.return_value = 5  

现在修补 sys.modules,并且可以在 with 块中有条件地导入 mysuper_calc3

>>> with patch.dict('sys.modules',{'mycalc': mock}):
...     from mysuper_calc import SuperCalc
...     if version_info[0] == 3:
...         from mysuper_calc3 import SuperCalc

calc/doctest/mysuper_calc_doctest.txt

>>> from sys import version_info
>>> from mock import Mock, patch
>>> mock = Mock(name='mycalc')
>>> mock.Calc.add.return_value = 5

>>> with patch.dict('sys.modules',{'mycalc': mock}):
...     from mysuper_calc import SuperCalc
...     if version_info[0] == 3:
...         from mysuper_calc3 import SuperCalc
>>> c = SuperCalc()
>>> c.add(2,3)
5

文件 mysuper_calc_doctest.txt 必须单独放在自己的目录里,否则 nosetests 会在非测试模块中搜索 doctest

PYTHONPATH=.. nosetests --with-doctest --doctest-extension=txt --verbosity=3

文档测试: mysuper_calc_doctest.txt ... 成功


运行了 1 个测试,耗时 0.038 秒

成功

这是一个包裹在 nosetests 周围的工具,用来检测 Python 3,并将没有语法错误的 .py 文件传递给 nosetests

mynosetests.py

import sys
from subprocess import Popen, PIPE
from glob import glob

f_list = []

py_files = glob('*py')
try:
    py_files.remove(sys.argv[0])
except ValueError:
    pass

for py_file in py_files:
    try:
        exec open(py_file)
    except SyntaxError:
        continue
    else:
        f_list.append(py_file)

proc = Popen(['nosetests'] + sys.argv[1:] + f_list,stdout=PIPE, stderr=PIPE)
print('%s\n%s' % proc.communicate())
sys.exit(proc.returncode)

撰写回答