面向用户和开发者的 IPython 测试#

注意

这完全复制自旧的 IPython wiki,目前正在开发中。本开发指南的许多信息已过时。

概述#

所有贡献给 IPython 的代码都必须有测试,这一点极其重要。测试应该以单元测试、doctest 或 IPython 测试系统可以检测到的其他实体形式编写。有关这方面的更多详细信息,请参阅下文。

IPython 中的每个子包都应该有自己的 tests 目录,其中包含该子包的所有测试。 tests 目录中的所有文件都应该包含“tests”一词,以便测试框架能够找到它们。

在文档字符串中,可以而且应该包含示例(无论是使用 IPython 提示符,如 In [1]:,还是“经典”Python 的 >>> 提示符)。测试系统将它们检测为 doctest 并运行它们;如果示例旨在提供信息但显示不可重现的信息(如文件系统数据),它允许控制跳过特定 doctest 的部分或全部。

如果子包除了 Python 标准库之外还有其他依赖项,则如果找不到这些依赖项,应该跳过该子包的测试。这非常重要,这样用户就不会因为缺少依赖项而导致测试失败。

我们使用的测试系统是 nose 测试运行器的扩展。特别是,我们开发了一个 nose 插件,允许我们逐字粘贴 IPython 会话并将其作为 doctest 进行测试,这对于我们来说极其重要。

运行测试套件#

您可以在下载源代码的目录中运行 IPython,甚至无需在系统范围内安装它或进行任何配置,只需在终端中输入

python2 -c "import IPython; IPython.start_ipython();"

要启动基于网络的 notebook,您可以使用

python2 -c "import IPython; IPython.start_ipython(['notebook']);"

为了运行测试套件,您至少必须能够导入 IPython,即使您尚未完全安装面向用户的脚本(在开发环境中很常见)。然后您可以使用以下命令运行测试

python -c "import IPython; IPython.test()"

一旦您通过完整安装或使用以下命令安装了 IPython

python setup.py develop

您将有一个名为 iptest 的系统范围脚本,它将运行完整的测试套件。然后您可以使用以下命令运行该套件

iptest  [args]

默认情况下,这会排除 IPython.parallel 相对较慢的测试。要运行这些测试,请使用 iptest --all

请注意,iptest 工具将针对 Python 解释器导入的代码运行测试。如果之前运行过命令 python setup.py symlink,那么这将始终是通过符号链接在本地目录中的测试代码。但是,如果尚未为正在测试的 Python 版本运行此命令,则 iptest 可能会针对已安装的 IPython 版本运行测试。

无论您如何运行,最终都应该看到类似以下内容

**********************************************************************
Test suite completed for system with the following information:
{'commit_hash': '144fdae',
 'commit_source': 'repository',
 'ipython_path': '/home/fperez/usr/lib/python2.6/site-packages/IPython',
 'ipython_version': '0.11.dev',
 'os_name': 'posix',
 'platform': 'Linux-2.6.35-22-generic-i686-with-Ubuntu-10.10-maverick',
 'sys_executable': '/usr/bin/python',
 'sys_platform': 'linux2',
 'sys_version': '2.6.6 (r266:84292, Sep 15 2010, 15:52:39) \n[GCC 4.4.5]'}

Tools and libraries available at test time:
   curses matplotlib pymongo qt sqlite3 tornado wx wx.aui zmq

Ran 9 test groups in 67.213s

Status:
OK

如果没有,将有一条消息指示哪个测试组失败以及如何单独重新运行该组。例如,这会测试 IPython.utils 子包,-v 选项显示进度指示器

$ iptest IPython.utils -- -v
..........................SS..SSS............................S.S...
.........................................................
----------------------------------------------------------------------
Ran 125 tests in 0.119s

OK (SKIP=7)

因为 IPython 测试机制基于 nose,所以您可以使用所有 nose 语法。 -- 之后的选项会传递给 nose。例如,这允许您运行 test_magic 模块中特定的测试 test_rehashx

$ iptest IPython.core.tests.test_magic:test_rehashx -- -vv
IPython.core.tests.test_magic.test_rehashx(True,) ... ok
IPython.core.tests.test_magic.test_rehashx(True,) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.100s

OK

在开发时,nose 的 --pdb--pdb-failures 特别有用,它们分别在错误或失败发生时将您带入交互式 pdb 会话:iptest mymodule -- --pdb

上面打印的系统信息摘要可以从顶级包访问。如果您遇到 IPython 的问题,在邮件列表中报告时包含此信息很有用;请使用

.. code:: python

from IPython import sys_info print sys_info()

并将生成的信息包含在您的查询中。

测试拉取请求#

我们有一个脚本,可以从 Github 获取拉取请求,将其与 master 合并,并在不同版本的 Python 上运行测试套件。这使用了一个单独的仓库副本,因此您可以在它运行时继续处理代码。要运行它

python tools/test_pr.py -p 1234

该数字是 Github 上的拉取请求号; -p 标志使其将结果发布到拉取请求的评论中。任何其他参数都会传递给 iptest

这需要 requestskeyring 包。

对于开发者:编写测试#

IPython 现在已经有了一个相当完整的测试套件,所以查看可用内容的最佳方法是查看大多数子包中的 tests 目录。但这里有一些提示可以简化这个过程。

主要工具:IPython.testing#

IPython.testing 包是所有用于测试 IPython(而不是其各个部分的测试)的机制所在。特别是,其中的 iptest 模块拥有控制测试过程的所有智能。在那里,make_exclude 函数用于构建一个排除黑名单,这些模块甚至不会被导入进行测试。这很重要,这样那些由于缺少依赖项而无法导入的东西就不会给最终用户带来错误,正如我们上面所说的。

decorators 模块包含许多有用的装饰器,特别有助于标记在某些条件下应跳过的单独测试(而不是因为缺少主要依赖项而完全将包列入黑名单)。

我们用于 doctest 的 nose 插件#

测试中的 plugin 子包包含一个名为 ipdoctest 的 nose 插件,它让 nose 了解 IPython 语法,因此您可以使用 IPython 提示符编写 doctest。您还可以用 # random 标记 doctest 输出,以便忽略与单个输入对应的输出(比使用省略号更强大,并且有助于将其保留为示例)。如果您希望执行整个文档字符串,但不检查任何输入的所有输出,则可以使用 # all-random 标记。 IPython.testing.plugin.dtexample 模块包含如何使用这些的示例;作为参考,下面是如何使用 # random

def ranfunc():
"""A function with some random output.

   Normal examples are verified as usual:
   >>> 1+3
   4

   But if you put '# random' in the output, it is ignored:
   >>> 1+3
   junk goes here...  # random

   >>> 1+2
   again,  anything goes #random
   if multiline, the random mark is only needed once.

   >>> 1+2
   You can also put the random marker at the end:
   # random

   >>> 1+2
   # random
   .. or at the beginning.

   More correct input is properly verified:
   >>> ranfunc()
   'ranfunc'
"""
return 'ranfunc'

以及 # all-random 的示例

def random_all():
"""A function where we ignore the output of ALL examples.

Examples:

  # all-random

  This mark tells the testing machinery that all subsequent examples
  should be treated as random (ignoring their output).  They are still
  executed, so if a they raise an error, it will be detected as such,
  but their output is completely ignored.

  >>> 1+3
  junk goes here...

  >>> 1+3
  klasdfj;

In [8]: print 'hello'
world  # random

In [9]: iprand()
Out[9]: 'iprand'
"""
return 'iprand'

在编写文档字符串时,您可以使用 @skip_doctest 装饰器来指示文档字符串根本不应被视为 doctest。 # all-random@skip_doctest 的区别在于,前者执行示例但忽略输出,而后者不执行任何代码。 @skip_doctest 应该用于示例纯粹是信息性的文档字符串。

如果某个文档字符串在特定条件下失败,但除此之外是一个很好的 doctest,您可以使用以下代码,它依赖于“null”装饰器来保持文档字符串完整,因为它在那里可以作为测试

# The docstring for full_path doctests differently on win32 (different path
# separator) so just skip the doctest there, and use a null decorator
# elsewhere:

doctest_deco = dec.skip_doctest if sys.platform == 'win32' else dec.null_deco

@doctest_deco
def full_path(startPath,files):
    """Make full paths for all the listed files, based on startPath..."""

    # function body follows...

通过我们理解 IPython 语法的 nose 插件,编写测试的一个极其有效的方法是简单地将交互式会话复制粘贴到文档字符串中。您可以通过在函数名称前加上 doctest_ 并在其主体中除了文档字符串外,绝对空无一物来编写这种类型的测试,其中您的文档字符串作为测试。在 IPython.core.tests.test_magic 中可以找到几个这样的示例,但为了完整起见,您的代码应该看起来像这样(一个简单的情况)

def doctest_time():
"""
In [10]: %time None
CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s
Wall time: 0.00 s
"""

此函数仅分析其文档字符串,但不被视为单独的测试,这就是其主体应为空的原因。

JavaScript 测试#

我们目前使用 casperjs 来测试 notebook 的 JavaScript 用户界面。

要单独运行 JS 测试套件,您可以使用 iptest js,它将启动一个新的 notebook 服务器并对其进行测试,或者您可以自己打开一个 notebook 服务器,然后

cd IPython/html/tests/casperjs;
casperjs test --includes=util.js test_cases

如果您的测试 notebook 服务器使用的端口不是默认端口 (8888),您还需要将其作为参数传递给测试套件。

casperjs test --includes=util.js --port=8889 test_cases

运行单个测试#

为了加快开发速度,您通常一次只处理一个测试。为此,只需将文件名直接传递给 casperjs test 命令,如下所示

casperjs test --includes=util.js  test_cases/execute_code_cell.js

理解 JavaScript 中的 JavaScript:#

CasperJS 是一个用 javascript 编写的浏览器,所以我们编写 javascript 代码来驱动它。Casper 浏览器本身也包含一个 javascript 实现(类似于 Firefox 和 Chrome 中自带的),在测试套件中,我们通过 this.evaluate 及其变体 (this.theEvaluate 等) 访问它们。此外,由于一切都是异步/回调性质的,因此有许多 this.then 调用来定义测试套件中的步骤。部分原因是因为每个步骤都有一个超时(默认为 5 或 10 秒)。此外,util.js 中已经有方便的函数可以帮助您等待给定单元格中的输出等。在我们的 javascript 测试中,如果您看到看起来像 look_like_pep8_naming_convention 的函数,那些可能来自 util.js,而来自 casper 的函数则 haveCamelCaseNamingConvention

test_cases 中的每个文件都看起来像这样(这是 test_cases/check_interrupt.js

casper.notebook_test(function () {
    this.evaluate(function () {
        var cell = IPython.notebook.get_cell(0);
        cell.set_text('import time\nfor x in range(3):\n    time.sleep(1)');
        cell.execute();
    });


    // interrupt using menu item (Kernel -> Interrupt)
    this.thenClick('li#int_kernel');

    this.wait_for_output(0);

    this.then(function () {
        var result = this.get_output_cell(0);
        this.test.assertEquals(result.ename, 'KeyboardInterrupt', 'keyboard interrupt (mouseclick)');
    });

    // run cell 0 again, now interrupting using keyboard shortcut
    this.thenEvaluate(function () {
        cell.clear_output();
        cell.execute();
    });

    // interrupt using Ctrl-M I keyboard shortcut
    this.thenEvaluate( function() {
        IPython.utils.press_ghetto(IPython.utils.keycodes.I)
    });

    this.wait_for_output(0);

    this.then(function () {
        var result = this.get_output_cell(0);
        this.test.assertEquals(result.ename, 'KeyboardInterrupt', 'keyboard interrupt (shortcut)');
    });
});

有关如何将参数从 Casper 测试套件传递给客户端 JavaScript 的示例,请参阅 IPython/html/tests/casperjs/util.js 中的 casper.wait_for_output 实现

测试系统设计说明#

本节是一组关于 IPython 测试需求关键点的说明,这些说明在编写系统时被使用,并且应该在系统演进过程中保留以供参考。

要完整测试 IPython,需要修改 nose 和 doctest 的默认行为,因为 IPython 提示符无法识别 Python 输入,并且 IPython 允许用户输入非有效 Python 代码(例如 %magics!system commands)。

我们基本上需要能够测试以下类型的代码

    1. 包含普通测试的纯 Python 文件。这不是问题,因为只要它们符合 nose 用于识别测试的(灵活的)约定,Nose 就会检测到它们。

    1. 包含 doctest 的 Python 文件。这里,我们有两种可能性

  • 提示符是常用的 >>>,输入是纯 Python。

  • 提示符形式为 In [1]:,输入可以包含扩展的 IPython 表达式。

在第一种情况下,只要使用 --with-doctest 标志调用 Nose,Nose 就会识别 doctest。但第二种情况可能需要修改或编写一个新的、支持 IPython 的 Nose doctest 插件。

    1. 包含代码块的 ReStructuredText 文件。对于这种类型的文件,我们有三种不同的代码块可能性

  • 它们使用 >>> 提示符。

  • 它们使用 In [1]: 提示符。

  • 它们是没有任何提示符的纯 Python 代码的独立块。

前两种情况类似于上述情况 #2,只是在这种情况下,doctest 必须使用 docutils 从输入代码块中提取,而不是从 Python 文档字符串中提取。

在第三种情况下,我们必须有一个约定来区分用于执行的代码块与其他可能是 shell 代码片段或不打算运行的其他示例的代码块。一种可能性是假定所有缩进的代码块都用于执行,但要有一个特殊的 docutils 指令用于不应执行的输入。

对于我们将要执行的代码块,所使用的约定是它们将被调用,并且如果它们运行完成而没有引发错误,则被认为是成功的。这类似于 Nose 对独立测试函数所做的操作,通过放置断言或其他形式的引发异常的语句,可以创建既是文字示例又是轻量级测试的例子。

    1. 函数和方法文档字符串中包含 doctest 的扩展模块。目前 Nose 根本无法正确找到这些文档字符串,因为底层的 doctest DocTestFinder 对象在那里失败。与上述 #2 类似,文档字符串可以包含纯 Python 或 IPython 提示符。

在这些中,只有 3-c(带有独立代码块的 reST)目前尚未实现。