面向使用者和開發者的 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
。
對於開發者:編寫測試#
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
)。
我們基本上需要能夠測試以下型別的程式碼
包含普通測試的純 Python 檔案。這不是問題,因為只要它們符合 nose 用於識別測試的(靈活的)約定,Nose 就會檢測到它們。
包含 doctest 的 Python 檔案。這裡,我們有兩種可能性
提示符是常用的
>>>
,輸入是純 Python。提示符形式為
In [1]:
,輸入可以包含擴充套件的 IPython 表示式。
在第一種情況下,只要使用 --with-doctest
標誌呼叫 Nose,Nose 就會識別 doctest。但第二種情況可能需要修改或編寫一個新的、支援 IPython 的 Nose doctest 外掛。
包含程式碼塊的 ReStructuredText 檔案。對於這種型別的檔案,我們有三種不同的程式碼塊可能性
它們使用
>>>
提示符。它們使用
In [1]:
提示符。它們是沒有任何提示符的純 Python 程式碼的獨立塊。
前兩種情況類似於上述情況 #2,只是在這種情況下,doctest 必須使用 docutils 從輸入程式碼塊中提取,而不是從 Python 文件字串中提取。
在第三種情況下,我們必須有一個約定來區分用於執行的程式碼塊與其他可能是 shell 程式碼片段或不打算執行的其他示例的程式碼塊。一種可能性是假定所有縮排的程式碼塊都用於執行,但要有一個特殊的 docutils 指令用於不應執行的輸入。
對於我們將要執行的程式碼塊,所使用的約定是它們將被呼叫,並且如果它們執行完成而沒有引發錯誤,則被認為是成功的。這類似於 Nose 對獨立測試函式所做的操作,透過放置斷言或其他形式的引發異常的語句,可以建立既是文字示例又是輕量級測試的例子。
函式和方法文件字串中包含 doctest 的擴充套件模組。目前 Nose 根本無法正確找到這些文件字串,因為底層的 doctest DocTestFinder 物件在那裡失敗。與上述 #2 類似,文件字串可以包含純 Python 或 IPython 提示符。
在這些中,只有 3-c(帶有獨立程式碼塊的 reST)目前尚未實現。