测试指南#

简介#

在 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')

测试方法可能接受两个或更多参数;第一个 label 是一个字符串,指定应测试的内容,第二个 verbose 是一个整数,给出输出详细程度的级别。有关详细信息,请参阅文档字符串 numpy.testlabel 的默认值为“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 通过编写“仿佛一无所知”NumPy 内部实现的 C 扩展模块进行测试,仅使用官方的 C-API 接口。此类模块的示例包括:针对用户自定义 rational 数据类型(在 _rational_tests 中)的测试,以及二进制分发版中包含的 ufunc 机制测试(在 _umath_tests 中)。从 1.21 版本开始,您还可以在测试中编写 C 代码片段,这些片段将被本地编译成 C 扩展模块并加载到 Python 中。

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

根据函数片段列表 functions 构建并导入一个名为 modname 的 C 扩展模块。

参数:
functions片段列表

每个片段都是一个序列,包含函数名、调用约定和代码片段。

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,可以通过特殊参数自动使用。例如,特殊参数名称 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)

文档测试#

文档测试是一种方便的方式,可以记录函数的行为并同时允许测试该行为。交互式 Python 会话的输出可以包含在函数的文档字符串中,测试框架可以运行示例并将实际输出与预期输出进行比较。

可以通过向 test() 调用添加 doctests 参数来运行文档测试;例如,要运行 numpy.lib 的所有测试(包括文档测试)

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

文档测试的运行方式就像在一个新的 Python 实例中一样,该实例已执行了 import numpy as np。属于 NumPy 子包的测试将已导入该子包。例如,对于 numpy/linalg/tests/ 中的测试,将创建这样的命名空间:from numpy import linalg 已执行。

tests/#

我们不将代码和测试保存在同一个目录中,而是将给定子包的所有测试都放在一个 tests/ 子目录中。对于我们的示例,如果它不存在,则需要在 numpy/xxx/ 中创建一个 tests/ 目录。因此,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

要测试的包的完整路径。