文档测试中的稳健相对路径

Robust relative paths in doctests

我有一个将文件路径作为参数的函数。

output = process('path/to/file.txt')

我想知道我是否可以轻松地对这样的函数进行文档测试。我在源代码的某处提供了一个示例输入文件,我可以将输出与我的预期进行比较(一个字符串、一个 python 对象,或者可能是另一个文件的内容)。

问题是我测试中的路径必然是相对的。相对于调用脚本的工作目录。

这意味着文档字符串中的所有路径都必须知道测试套件的入口点。显然这并不理想。在更复杂的测试环境中,我可以使用 __file__ 使路径成为绝对路径,但在 doctest 中,__file__ 不存在。

以文件形式提供刺激时通常的设置是什么?

我希望听到一些比 'always run the test suite from the same working directory' 更好的解决方案。

编辑:我想运行 来自集中测试套件入口点的文档测试。

import doctest
import mymodule

doctest.testmod(mymodule)

由于您是在模块级别执行此操作,因此我假设您还 packaged your modules as a proper Python package 使用了 setuptools 之类的东西,并将其部署到某个环境中,然后将在该环境中执行测试。此外,在 __file__ 不存在的假设中你只是部分正确 - 它也没有为从压缩 Python egg 导入的模块定义(随着轮子成为 de facto 包装方法,但它们可以而且确实存在)。

有多种可能的方法,其复杂性和权衡各不相同,其是否有效取决于要测试的模块的结构。

1)(不推荐,但无论如何包括在内,因为有时这在最简单的示例中效果最好。)最懒惰但最稳定、独立和跨平台的方式 - 它假定文件打开方法仅在要测试的那个模块中完成并使用相同的调用完成(例如 open),extraglobs 参数的用法可用于替代 open 调用.例如

from io import StringIO
import doctest
import mymodule

files = {
    'path/to/file1.txt': '... contents of file1.txt ...', 
    'path/to/file2.txt': '... contents of file2.txt ...',
} 

def lazyopen(p, flag='r'):
    result = StringIO(files[p] if flag == 'r' else '')
    result.name = p 
    return result

doctest.testmod(mymodule, extraglobs={'open': lazyopen})

2) 创建一个真正的测试套件,而不是通过 doctest.testmod

使用内置测试套件

虽然 shorthand 很有用,但它太有限了,因为它是独立的,不能与可能构建的其他测试套件结合使用。考虑创建一个专用的测试模块(例如 mymodule/tests.py)。我通常更喜欢创建一个名为 mymodule/tests 的目录,其中的单元测试名为 test_mysubmodule.py,以及一个包含 test_suite 设置的 __init__.py,如

def make_suite():
    import mymodule
    import os

    def setUp(suite):
        suite.olddir = os.getcwd()  # save the current working directory
        os.chdir(targetdir)  # need to define targetdir

    def tearDown(suite):
        os.chdir(suite.olddir)  # restore the original working directory

    return doctest.DocTestSuite(mymodule, setUp=setUp, tearDown=tearDown)

所以我们已经涵盖了基本的,但是targetdir需要定义。同样,您可以考虑多种因素:

1) 创建一个临时目录并使用 setupos.chdir 将所需文件填充到该目录,然后删除 tearDown 中的临时目录。要么在测试模块中手动写入以字符串形式存储的数据,要么从您的项目中复制,要么从存档中提取,但我们如何真正获得这些数据?这导致...

2) 如果源文件在您的项目中,并且 setuptools 在环境中是 available/installed,只需使用 pkg_resources.resource_filename 获取位置,并分配 targetdir 到那个。 setUp 现在可能看起来像

    def setUp(suite):
        suite.olddir = os.getcwd()
        targetdir = pkg_resources.resource_filename('mymodule', '')
        os.chdir(targetdir)

此外,最后,由于现在这是一个真正的测试套件,由 mymodules.tests 中的 make_suite 函数生成,因此必须使用测试运行器来执行它,幸运的是,它已包含在内作为默认单元测试框架的一部分,作为一个简单的命令,可以像这样完成:

$ python -m unittest mymodule.tests.make_suite
.
----------------------------------------------------------------------
Ran 1 test in 0.014s

OK

此外,由于这是一个真正的测试套件,它可以与来自 unittest 模块的测试套件 globbing 集成,将所有内容组合成一个完整的测试套件,用于您的整个包。

def make_suite():
    # ... the other setup code

    # this loads all unittests in mymodule from `test_*.py` files
    # inside `mymodule.tests`
    test_suite = test_loader.discover(
        'mymodule.tests', pattern='test_*.py')
    test_suite.addTest(
        doctest.DocTestSuite(mymodule, setUp=setUp, tearDown=tearDown))
    return test_suite

同样,python -m unittest 命令可用于执行完整测试套件返回的测试。