为什么导入模块会破坏我的 doctest (Python 2.7)

Why is importing a module breaking my doctest (Python 2.7)

我试图在 class 中的 Python 2.7 程序中使用 StringIO instance in a doctest。我没有从测试中得到任何输出,而是得到了一个响应 "Got nothing"。

这个简化的测试用例演示了错误:

#!/usr/bin/env python2.7
# encoding: utf-8

class Dummy(object):
    '''Dummy: demonstrates a doctest problem

    >>> from StringIO import StringIO
    ... s = StringIO()
    ... print("s is created")
    s is created
    '''

if __name__ == "__main__":
    import doctest
    doctest.testmod()

预期行为:测试通过。

观察到的行为:测试失败,输出如下:

% ./src/doctest_fail.py
**********************************************************************
File "./src/doctest_fail.py", line 7, in __main__.Dummy
Failed example:
    from StringIO import StringIO
    s = StringIO()
    print("s is created")
Expected:
    s is created
Got nothing
**********************************************************************
1 items had failures:
   1 of   1 in __main__.Dummy
***Test Failed*** 1 failures.

为什么这个 doctest 失败了?为了能够在我的文档测试中使用类似 StringIO 的功能(带有文件接口的文字字符串),我需要进行哪些更改?

连续行语法 (...) 混淆了 doctest 解析器。这有效:

#!/usr/bin/env python2.7
# encoding: utf-8

class Dummy(object):
    '''Dummy: demonstrates a doctest problem

    >>> from StringIO import StringIO
    >>> s = StringIO()
    >>> print("s is created")
    s is created
    '''

if __name__ == "__main__":
    import doctest
    doctest.testmod()

[以 wim 的正确答案为基础,但更多地解释了原因,并查看了底层的 doctest 语义。]

该示例失败,因为它在单独的简单语句前面使用了 PS2 syntax (...) instead of PS1 语法 (>>>)。

...改为>>>:

#!/usr/bin/env python2.7
# encoding: utf-8

class Dummy(object):
    '''Dummy: demonstrates a doctest problem

    >>> from StringIO import StringIO
    >>> s = StringIO()
    >>> print("s is created")
    s is created
    '''

if __name__ == "__main__":
    import doctest
    doctest.testmod()

现在更正后的示例已重命名为 doctest_pass.py,运行时没有错误。它不产生任何输出,这意味着所有测试都通过了:

% src/doctest_pass.py                       

为什么 >>> 语法正确? doctest 的 Python 库参考,25.2.3.2. How are Docstring Examples Recognized? 应该是找到答案的地方,但对这种语法不是很清楚。

Doctest 扫描文档字符串,寻找 "Examples"。在它看到 PS1 字符串 >>> 的地方,它会将从那里到行尾的所有内容作为示例。它还将以下任何以 PS2 字符串 ... 开头的行附加到示例中(参见:_EXAMPLE_RE in class doctest.DocTestParser,第 584-595 行)。它采用后续行,直到下一个空白行或以 PS1 字符串开头的行,作为所需输出。

Doctest 使用 compile() built-in function in an exec statement 将每个示例编译为 Python "interactive statement"(参见:doctest.DocTestRunner.__run(),第 1314-1315 行)。

一个“interactive statement" is a statement list ending with a newline, or a Compound Statement。一个复合语句,例如一个iftry语句,"in general, […spans] multiple lines, although in simple incarnations a whole compound statement may be contained in one line."这是一个多行复合语句:

if 1 > 0:
    print("As expected")
else:
    print("Should not happen")

一个语句列表是一个或多个 simple statement 单行,用分号分隔。

from StringIO import StringIO
s = StringIO(); print("s is created")

因此,问题的 doctest 失败了,因为它包含一个包含三个简单语句的示例,并且没有分号分隔符。将 PS2 字符串更改为 PS1 字符串成功,因为它将文档字符串变成了三个示例的序列,每个示例都有一个简单的语句。尽管这三行一起工作以设置一项功能的一项测试,但它们并不是一个单一的测试夹具。它们是三个测试,其中两个设置状态但不真正测试主要功能。

顺便说一下,您可以通过使用 -v 标志查看 doctest 识别的示例数量。请注意,它说的是“3 tests in __main__.Dummy”。人们可能认为这三行是一个测试单元,但 doctest 看到了三个示例。前两个示例没有预期的输出。当示例执行并且没有生成输出时,这算作 "pass".

% src/doctest_pass.py -v
Trying:
    from StringIO import StringIO
Expecting nothing
ok
Trying:
    s = StringIO()
Expecting nothing
ok
Trying:
    print("s is created")
Expecting:
    s is created
ok
1 items had no tests:
    __main__
1 items passed all tests:
   3 tests in __main__.Dummy
3 tests in 2 items.
3 passed and 0 failed.
Test passed.

在单个文档字符串中,示例按顺序执行。每个示例的状态更改都保留在同一文档字符串中的以下示例中。因此 import 语句定义了一个模块名称,s = 赋值语句使用该模块名称并定义了一个变量名称,等等。 doctest 文档 25.2.3.3. What’s the Execution Context? 在说 "examples can freely use … names defined earlier in the docstring being run."

时间接地披露了这一点

该部分的前一句 "each time doctest finds a docstring to test, it uses a shallow copy of M’s globals, so that … one test in M can’t leave behind crumbs that accidentally allow another test to work" 有点误导。确实,M 中的一个测试不会影响另一个文档字符串中的测试。但是,在单个文档字符串中,较早的测试肯定会留下碎屑,这很可能会影响后面的测试。

为什么 Python 文档测试库参考 25.2.3.2. How are Docstring Examples Recognized? 中的示例显示了 ... 语法的示例?该示例显示了一个 if 语句,它是多行的复合语句。第二行和后续行用 PS2 字符串标记。