测试指南#
介绍#
直到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.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
有关测试的详细信息,请参见 测试构建
运行 doctests#
NumPy 文档包含代码示例,即”doctests”.为了检查示例是否正确,请安装 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
时,doctests 不会运行.
其他运行测试的方法#
编写你自己的测试#
如果你正在编写希望成为NumPy一部分的代码,请在开发代码时编写测试.NumPy包目录中的每个Python模块、扩展模块或子包都应该有一个相应的 test_<name>.py
文件.Pytest会检查这些文件中的测试方法(命名为 test*
)和测试类(命名为 Test*
).
假设你有一个包含函数 zzz()
的 NumPy 模块 numpy/xxx/yyy.py
.要测试这个函数,你可以创建一个名为 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
语句或专门的断言函数来测试某个假设是否有效.如果断言失败,测试就会失败.常见的断言函数包括:
numpy.testing.assert_equal
用于测试结果数组和参考数组之间的完全元素级相等性.numpy.testing.assert_allclose
用于测试结果数组与参考数组之间的近似元素相等性(即,具有指定的相对和绝对容差),并且numpy.testing.assert_array_less
用于测试(严格)结果数组和参考数组之间的元素顺序.
默认情况下,这些断言函数仅比较数组中的数值.可以考虑使用 strict=True
选项来检查数组的 dtype 和形状.
当你需要自定义断言时,使用 Python 的 assert
语句.注意 pytest
内部重写了 assert
语句,以便在失败时提供信息丰富的输出,因此它应该优于传统的变体 numpy.testing.assert_
.虽然普通的 assert
语句在以优化模式 -O
运行 Python 时会被忽略,但在使用 pytest 运行测试时这不是问题.
同样地,pytest 函数 pytest.raises
和 pytest.warns
应该优先于它们的传统对应物 numpy.testing.assert_raises
和 numpy.testing.assert_warns
,这些版本被更广泛地使用.这些版本还接受一个 match
参数,该参数应始终用于精确地定位预期的警告或错误.
注意 test_
函数或方法不应该有文档字符串,因为这使得在使用 verbose=2``(或类似的详细设置)运行测试套件时,很难从输出中识别测试.使用简单的注释(``#
)来描述测试的意图,并帮助不熟悉的读者解释代码.
此外,由于NumPy的大部分代码是遗留代码,最初编写时没有单元测试,因此仍有几个模块尚未进行测试.请随时选择其中一个模块并为其开发测试.
在测试中使用C代码#
NumPy 暴露了一个丰富的 C-API .这些是通过编写的 c-extension 模块进行测试的,这些模块”仿佛”对 NumPy 的内部一无所知,而是仅使用官方的 C-API 接口.这种模块的例子包括 _rational_tests
中用户定义的 rational
dtype 测试,或 _umath_tests
中的 ufunc 机制测试,它们是二进制分发的一部分.从版本 1.21 开始,你还可以在测试中编写 C 代码片段,这些代码将在本地编译成 c-extension 模块并加载到 python 中.
- numpy.testing.extbuild.build_and_import_extension(modname, functions, *, prologue='', build_dir=None, include_dirs=[], more_init='')#
构建并导入一个c扩展模块 modname 从函数片段列表 functions 中.
- 参数:
- functions片段列表
每个片段是一个包含 func_name、调用约定、代码片段的序列.
- prologuestring
代码在其余部分之前,通常是额外的
#include
或#define
宏.- build_dirpathlib.Path
构建模块的位置,通常是一个临时目录
- include_dirs列表
编译时查找包含文件的额外目录
- more_initstring
模块中出现的代码 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')
设置和拆卸函数到函数和方法被称为”固定装置”,它们应该谨慎使用.``pytest`` 支持在各种范围内更通用的固定装置,可以通过特殊参数自动使用.例如,特殊参数名称 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 中,测试框架可以运行示例并比较实际输出与预期输出.
可以通过在 test()
调用中添加 doctests
参数来运行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__.py
和 setup.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):
...
当然,一个测试可以通过分别使用 skip
或 xfail
而无需参数来无条件跳过或标记为已知失败.
跳过和已知失败的测试总数在测试运行结束时显示.跳过的测试在测试结果中标记为 'S'
(或 'SKIPPED'
如果 verbose > 1
),已知失败的测试标记为 'x'
(或 'XFAIL'
如果 verbose > 1
).
随机数据的测试#
对随机数据的测试是好的,但由于测试失败旨在暴露新的错误或回归,一个大部分时间通过但在没有代码更改的情况下偶尔失败的测试是没有帮助的.通过在生成随机数据之前设置随机数种子使其确定性.根据随机数的来源,使用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
要测试的包的完整路径.