測試指南#

簡介#

在 1.15 版本之前,NumPy 使用 nose 測試框架,現在改用 pytest 框架。舊框架仍然維護,以支援使用舊 NumPy 框架的下游專案,但 NumPy 的所有測試都應使用 pytest。

我們的目標是 NumPy 中的每個模組和套件都應具有詳盡的單元測試集。這些測試應涵蓋給定常式的完整功能,以及其對於錯誤或非預期輸入引數的穩健性。設計良好的測試具有良好的覆蓋率,對於重構的便利性有極大的影響。每當在常式中發現新的錯誤時,您都應該為該特定案例編寫新的測試,並將其新增到測試套件中,以防止該錯誤在未被注意到的情況下再次出現。

注意

SciPy 使用來自 numpy.testing 的測試框架,因此以下顯示的所有 NumPy 範例也適用於 SciPy

測試 NumPy#

NumPy 可以透過多種方式進行測試,請選擇您覺得舒適的任何方式。

從 Python 內部執行測試#

您可以透過 numpy.test 測試已安裝的 NumPy,例如,若要執行 NumPy 的完整測試套件,請使用以下命令

>>> import numpy
>>> numpy.test(label='slow')

test 方法可以接受兩個或多個引數;第一個 label 是一個字串,指定應測試的內容,第二個 verbose 是一個整數,指定輸出詳細程度的級別。請參閱 numpy.test 的文件字串以取得詳細資訊。label 的預設值為 ‘fast’ - 這將執行標準測試。字串 ‘full’ 將執行完整的測試組,包括那些被識別為執行緩慢的測試。如果 verbose 為 1 或更小,則測試只會顯示關於正在執行的測試的資訊訊息;但如果它大於 1,則測試還將提供關於遺失測試的警告。因此,如果您想要執行每個測試並取得關於哪些模組沒有測試的訊息

>>> numpy.test(label='full', verbose=2)  # or numpy.test('full', 2)

最後,如果您只對測試 NumPy 的子集感興趣,例如 _core 模組,請使用以下命令

>>> numpy._core.test()

從命令列執行測試#

如果您想要建置 NumPy 以便處理 NumPy 本身,請使用 spin 工具。若要執行 NumPy 的完整測試套件

$ spin test -m full

測試 NumPy 的子集

$ spin test -t numpy/_core/tests

有關測試的詳細資訊,請參閱 測試建置

執行 doctest#

NumPy 文件包含程式碼範例,「doctest」。若要檢查範例是否正確,請安裝 scipy-doctest 套件

$ pip install scipy-doctest

並執行以下其中一個

$ spin check-docs -v
$ spin check-docs numpy/linalg
$ spin check-docs -- -k 'det and not slogdet'

請注意,當您使用 spin test 時,不會執行 doctest。

其他執行測試的方法#

使用您最喜歡的 IDE 執行測試,例如 vscodepycharm

編寫您自己的測試#

如果您要編寫希望成為 NumPy 一部分的程式碼,請在開發程式碼時同時編寫測試。NumPy 套件目錄中的每個 Python 模組、擴充模組或子套件都應具有對應的 test_<name>.py 檔案。Pytest 會檢查這些檔案中的測試方法(命名為 test*)和測試類別(命名為 Test*)。

假設您有一個 NumPy 模組 numpy/xxx/yyy.py,其中包含一個函數 zzz()。若要測試此函數,您將建立一個名為 test_yyy.py 的測試模組。如果您只需要測試 zzz 的一個方面,您可以簡單地新增一個測試函數

def test_zzz():
    assert zzz() == 'Hello from zzz'

更常見的情況是,我們需要將多個測試組合在一起,因此我們建立一個測試類別

import pytest

# import xxx symbols
from numpy.xxx.yyy import zzz
import pytest

class TestZzz:
    def test_simple(self):
        assert zzz() == 'Hello from zzz'

    def test_invalid_parameter(self):
        with pytest.raises(ValueError, match='.*some matching regex.*'):
            ...

在這些測試方法中,assert 陳述式或專用的斷言函數用於測試特定假設是否有效。如果斷言失敗,則測試失敗。常見的斷言函數包括

預設情況下,這些斷言函數僅比較陣列中的數值。考慮使用 strict=True 選項來同時檢查陣列 dtype 和形狀。

當您需要自訂斷言時,請使用 Python assert 陳述式。請注意,pytest 在內部會重寫 assert 陳述式,以便在失敗時提供資訊豐富的輸出,因此應優先於舊版的 numpy.testing.assert_ 變體。雖然在以最佳化模式 (-O) 執行 Python 時,會忽略純 assert 陳述式,但在使用 pytest 執行測試時,這不是問題。

同樣地,pytest 函數 pytest.raisespytest.warns 應優先於其舊版的對應項 numpy.testing.assert_raisesnumpy.testing.assert_warns,這些對應項的使用範圍更廣。這些版本也接受 match 參數,應始終使用該參數來精確鎖定預期的警告或錯誤。

請注意,test_ 函數或方法不應具有文件字串,因為這會使得難以從以 verbose=2 (或類似的詳細程度設定) 執行測試套件的輸出中識別測試。使用純註解 (#) 來描述測試的意圖,並協助不熟悉的讀者理解程式碼。

此外,由於 NumPy 的大部分是最初在沒有單元測試的情況下編寫的舊版程式碼,因此仍然有幾個模組尚未進行測試。請隨時選擇其中一個模組並為其開發測試。

在測試中使用 C 程式碼#

NumPy 公開了豐富的 C-API。這些 API 使用 c 擴充模組進行測試,這些模組的編寫方式「彷彿」它們對 NumPy 的內部結構一無所知,而是僅使用官方的 C-API 介面。此類模組的範例包括 _rational_tests 中使用者定義的 rational dtype 的測試,或二進位發行版本中的 _umath_tests 中的 ufunc 機制測試。從 1.21 版本開始,您也可以在測試中編寫 C 程式碼片段,這些片段將在本機編譯為 c 擴充模組並載入到 Python 中。

numpy.testing.extbuild.build_and_import_extension(modname, functions, *, prologue='', build_dir=None, include_dirs=[], more_init='')#

從函數片段 functions 的清單建置並匯入 c 擴充模組 modname

參數:
functions片段清單

每個片段都是 func_name、呼叫慣例、程式碼片段的序列。

prologue字串

位於其餘程式碼之前的程式碼,通常是額外的 #include#define 巨集。

build_dirpathlib.Path

模組的建置位置,通常是暫存目錄

include_dirs清單

編譯時尋找標頭檔的額外目錄

more_init字串

將出現在模組 PyMODINIT_FUNC 中的程式碼

傳回值:
out: 模組

模組將已載入並可供使用

範例

>>> functions = [("test_bytes", "METH_O", """
    if ( !PyBytesCheck(args)) {
        Py_RETURN_FALSE;
    }
    Py_RETURN_TRUE;
""")]
>>> mod = build_and_import_extension("testme", functions)
>>> assert not mod.test_bytes('abc')
>>> assert mod.test_bytes(b'abc')

標記測試#

未標記的測試 (如上面的測試) 在預設的 numpy.test() 執行中執行。如果您想要將測試標記為緩慢 - 因此保留用於完整的 numpy.test(label='full') 執行,您可以使用 pytest.mark.slow 標記它

import pytest

@pytest.mark.slow
def test_big(self):
    print('Big, slow test')

方法也是如此

class test_zzz:
    @pytest.mark.slow
    def test_simple(self):
        assert_(zzz() == 'Hello from zzz')

更輕鬆的設定和拆解函數/方法#

測試會依名稱尋找模組層級或類別方法層級的設定和拆解函數;因此

def setup_module():
    """Module-level setup"""
    print('doing setup')

def teardown_module():
    """Module-level teardown"""
    print('doing teardown')


class TestMe:
    def setup_method(self):
        """Class-level setup"""
        print('doing setup')

    def teardown_method():
        """Class-level teardown"""
        print('doing teardown')

函數和方法的設定和拆解函數稱為「fixture」,它們應謹慎使用。pytest 支援各種範圍的更通用的 fixture,這些 fixture 可以透過特殊引數自動使用。例如,特殊引數名稱 tmpdir 在測試中用於建立暫存目錄。

參數化測試#

pytest 的一個非常好的功能是使用 pytest.mark.parametrize 裝飾器輕鬆跨一系列參數值進行測試。例如,假設您想要針對三種陣列大小和兩種資料類型的所有組合測試 linalg.solve

@pytest.mark.parametrize('dimensionality', [3, 10, 25])
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
def test_solve(dimensionality, dtype):
    np.random.seed(842523)
    A = np.random.random(size=(dimensionality, dimensionality)).astype(dtype)
    b = np.random.random(size=dimensionality).astype(dtype)
    x = np.linalg.solve(A, b)
    eps = np.finfo(dtype).eps
    assert_allclose(A @ x, b, rtol=eps*1e2, atol=0)
    assert x.dtype == np.dtype(dtype)

Doctest#

Doctest 是一種方便的方式,可以記錄函數的行為,並同時測試該行為。互動式 Python 會話的輸出可以包含在函數的文件字串中,測試框架可以執行範例,並將實際輸出與預期輸出進行比較。

可以透過將 doctests 引數新增至 test() 呼叫來執行 doctest;例如,若要執行 numpy.lib 的所有測試 (包括 doctest)

>>> import numpy as np
>>> np.lib.test(doctests=True)

doctest 的執行方式就像它們在新的 Python 執行個體中,該執行個體已執行 import numpy as np。屬於 NumPy 子套件一部分的測試將已匯入該子套件。例如,對於 numpy/linalg/tests/ 中的測試,將建立命名空間,以便 from numpy import linalg 已執行。

tests/#

我們沒有將程式碼和測試放在同一個目錄中,而是將給定子套件的所有測試放在 tests/ 子目錄中。對於我們的範例,如果 tests/ 目錄尚不存在,您將需要在 numpy/xxx/ 中建立一個。因此,test_yyy.py 的路徑是 numpy/xxx/tests/test_yyy.py

一旦編寫了 numpy/xxx/tests/test_yyy.py,就可以透過前往 tests/ 目錄並輸入以下命令來執行測試

python test_yyy.py

或者,如果您將 numpy/xxx/tests/ 新增至 Python 路徑,您可以像這樣在解譯器中以互動方式執行測試

>>> import test_yyy
>>> test_yyy.test()

__init__.pysetup.py#

但是,通常不希望將 tests/ 目錄新增至 python 路徑。相反地,最好直接從模組 xxx 叫用測試。為此,只需將以下幾行程式碼放在套件的 __init__.py 檔案末尾

...
def test(level=1, verbosity=1):
    from numpy.testing import Tester
    return Tester().test(level, verbosity)

您還需要在 setup.py 的配置區段中新增 tests 目錄

...
def configuration(parent_package='', top_path=None):
    ...
    config.add_subpackage('tests')
    return config
...

現在您可以執行以下操作來測試您的模組

>>> import numpy
>>> numpy.xxx.test()

此外,當叫用整個 NumPy 測試套件時,將會找到並執行您的測試

>>> import numpy
>>> numpy.test()
# your tests are included and run automatically!

提示與技巧#

已知失敗與略過測試#

有時您可能想要略過測試或將其標記為已知失敗,例如,當測試套件在程式碼 (旨在測試的程式碼) 之前編寫時,或者如果測試僅在特定架構上失敗。

若要略過測試,只需使用 skipif

import pytest

@pytest.mark.skipif(SkipMyTest, reason="Skipping this test because...")
def test_something(foo):
    ...

如果 SkipMyTest 的評估結果為非零值,則測試會標記為略過,而詳細測試輸出中的訊息是提供給 skipif 的第二個引數。同樣地,可以使用 xfail 將測試標記為已知失敗

import pytest

@pytest.mark.xfail(MyTestFails, reason="This test is known to fail because...")
def test_something_else(foo):
    ...

當然,可以透過使用不帶引數的 skipxfail 分別無條件略過測試或將測試標記為已知失敗。

在測試執行結束時,會顯示略過和已知失敗測試的總數。略過的測試在測試結果中標記為 'S' (或對於 verbose > 1 標記為 'SKIPPED'),而已知失敗的測試標記為 'x' (如果 verbose > 1 則標記為 'XFAIL')。

隨機資料的測試#

隨機資料的測試很好,但由於測試失敗旨在揭露新的錯誤或迴歸,因此大多數時間通過但偶爾在沒有程式碼變更的情況下失敗的測試沒有幫助。透過在產生隨機資料之前設定隨機數種子,使隨機資料具有確定性。使用 Python 的 random.seed(some_number) 或 NumPy 的 numpy.random.seed(some_number),取決於隨機數的來源。

或者,您可以使用 Hypothesis 來產生任意資料。Hypothesis 會為您管理 Python 和 Numpy 的隨機種子,並提供一種非常簡潔且強大的方式來描述資料 (包括 hypothesis.extra.numpy,例如用於一組可相互廣播的形狀)。

相較於隨機產生,優點包括用於重播和共用失敗而無需固定種子的工具、報告每個失敗的最小範例,以及比天真隨機技術更好的觸發錯誤技術。

numpy.test 的文件#

numpy.test(label='fast', verbose=1, extra_argv=None, doctests=False, coverage=False, durations=-1, tests=None)#

Pytest 測試執行器。

測試函數通常會新增至套件的 __init__.py,如下所示

from numpy._pytesttester import PytestTester
test = PytestTester(__name__).test
del PytestTester

呼叫此測試函數會尋找並執行與模組及其所有子模組相關聯的所有測試。

參數:
module_name模組名稱

要測試的模組名稱。

註解

與先前的基於 nose 的實作不同,此類別不會公開公開,因為它會執行一些 numpy 特定的警告抑制。

屬性:
module_namestr

要測試的套件的完整路徑。