测试用户和开发者的 IPython#

注意

这是从旧的 IPython wiki 直接复制过来的,目前正在开发中。开发指南的这一部分中的许多信息已经过时。

概述#

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

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

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

如果一个子包有超出Python标准库的任何依赖项,那么如果找不到这些依赖项,则应跳过该子包的测试。这一点非常重要,这样用户就不会因为缺少依赖项而导致测试失败。

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

运行测试套件#

你可以通过在终端输入以下命令,从源代码下载目录运行 IPython,而无需在系统范围内安装它或进行任何配置:

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

要启动基于Web的笔记本,您可以使用:

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获取拉取请求,将其与主分支合并,并在不同版本的Python上运行测试套件。这使用了一个单独的仓库副本,因此你可以在运行时继续处理代码。要运行它:

python tools/test_pr.py -p 1234

数字是来自Github的拉取请求编号;-p``标志使其将结果发布到拉取请求的评论中。任何进一步的参数都会传递给``iptest

这需要 requestskeyring 包。

对于开发者:编写测试#

到目前为止,IPython 有一个合理的测试套件,所以查看大多数子包中的 tests 目录是了解可用内容的最佳方式。但这里有一些提示,使这个过程更容易。

主要工具:IPython.testing#

IPython.testing 包是所有用于测试 IPython 的机制(而不是其各个部分的测试)所在的地方。特别是,其中的 iptest 模块拥有控制测试过程的所有智能。在该模块中,make_exclude 函数用于构建一个排除列表,这些是被排除的模块,它们在测试中甚至不会被导入。这一点很重要,因为如上所述,由于缺少依赖项而无法导入的内容不会向最终用户抛出错误。

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

我们用于 doctests 的 nose 插件#

在测试中的 plugin 子包包含一个名为 ipdoctest 的 nose 插件,它让 nose 了解 IPython 语法,因此您可以使用 IPython 提示符编写 doctests。您还可以使用 # random 标记 doctest 输出,以忽略与单个输入对应的输出(比使用省略号更强,并且有助于将其保留为示例)。如果您希望执行整个 docstring,但不检查任何输入的输出,可以使用 # 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 插件,编写测试的一个非常有效的方法是将交互式会话直接复制粘贴到 docstring 中。你可以编写这种类型的测试,其中你的 docstring 仅作为测试使用,只需在函数名前加上 doctest_ 并将函数体除了 docstring 外 完全留空。在 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 来测试笔记本的 JavaScript 用户界面。

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

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

如果你的测试笔记本服务器使用的是默认端口(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 测试中,如果你看到符合 pep8命名约定 的函数,那些可能来自 util.js,而那些带有 驼峰命名约定 的函数则是 Casper 自带的。

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. 包含 doctests 的 Python 文件。这里,我们有两个可能性:

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

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

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

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

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

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

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

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

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

对于我们将执行的代码块,使用的约定是它们被调用并且在不引发错误的情况下运行完成即被视为成功。这与 Nose 对独立测试函数的做法类似,通过放置断言或其他形式的引发异常的语句,可以拥有既是说明性示例又是轻量级测试的代码。

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

其中,只有 3-c(带有独立代码块的 reST)在此时未实现。