测试指南#

介绍#

在 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 是一个整数,用于提供输出详细程度.有关详细信息,请参见 docstring 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=None, more_init='')#

从函数片段列表"functions"构建并导入 c 扩展模块"modname".

参数:
functions片段列表

每个片段都是 func_name,调用约定,snippet 的序列.

prologue字符串

位于其余代码之前的代码,通常是额外的 #include#define 宏.

build_dirpathlib.Path

构建模块的位置,通常是临时目录

include_dirs列表

编译时查找包含文件的额外目录

more_init字符串

将出现在模块 PyMODINIT_FUNC 中的代码

返回:
out: module

该模块已被加载并可以使用

示例

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

更简单的 setup 和 teardown 函数/方法#

测试通过名称查找模块级别或类方法级别的 setup 和 teardown 函数;因此:

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

函数和方法的 Setup 和 teardown 函数被称为"fixtures",应谨慎使用. 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)

Doctests#

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

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

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

运行 doctests 时,就好像它们在一个新的 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 的配置部分添加测试目录:

...
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

要测试的包的完整路径.