测试指南#
简介#
在 1.15 版本发布之前,NumPy 使用的是 nose 测试框架,现在使用的是 pytest 框架.旧的框架仍然被维护,以支持使用旧的 numpy 框架的下游项目,但是 NumPy 的所有测试都应该使用 pytest.
我们的目标是 NumPy 中的每个模块和包都应该有一套完整的单元测试.这些测试应该行使给定例程的全部功能,以及它对错误或意外输入参数的鲁棒性.良好设计的测试和良好的覆盖率对于重构的便利性有很大的影响.每当在例程中发现新 bug 时,您应该为该特定情况编写新的测试,并将其添加到测试套件中,以防止该 bug 在不知不觉中再次出现.
备注
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 文档包含代码示例,”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 时,不会运行 doctest.
运行测试的其他方法#
编写您自己的测试#
如果您正在编写希望成为 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 语句或专门的断言函数用于测试某个假设是否有效.如果声明失败,则测试失败.常见的断言函数包括:
–END–
numpy.testing.assert_equalfor testing exact elementwise equality between a result array and a reference,numpy.testing.assert_allclosefor testing near elementwise equality between a result array and a reference (i.e. with specified relative and absolute tolerances), andnumpy.testing.assert_array_lessfor testing (strict) elementwise ordering between a result array and a reference.
By default, these assertion functions only compare the numerical values in the
arrays. Consider using the strict=True option to check the array dtype
and shape, too.
When you need custom assertions, use the Python assert statement. Note that
pytest internally rewrites assert statements to give informative
output when it fails, so it should be preferred over the legacy variant
numpy.testing.assert_. Whereas plain assert statements are ignored
when running Python in optimized mode with -O, this is not an issue when
running tests with pytest.
Similarly, the pytest functions pytest.raises and pytest.warns
should be preferred over their legacy counterparts
numpy.testing.assert_raises and numpy.testing.assert_warns,
which are more broadly used. These versions also accept a match
parameter, which should always be used to precisely target the intended
warning or error.
Note that test_ functions or methods should not have a docstring, because
that makes it hard to identify the test from the output of running the test
suite with verbose=2 (or similar verbosity setting). Use plain comments
(#) to describe the intent of the test and help the unfamiliar reader to
interpret the code.
Also, since much of NumPy is legacy code that was originally written without unit tests, there are still several modules that don’t have tests yet. Please feel free to choose one of these modules and develop tests for it.
Using C code in tests#
NumPy exposes a rich C-API . These are tested using c-extension
modules written “as-if” they know nothing about the internals of NumPy, rather
using the official C-API interfaces only. Examples of such modules are tests
for a user-defined rational dtype in _rational_tests or the ufunc
machinery tests in _umath_tests which are part of the binary distribution.
Starting from version 1.21, you can also write snippets of C code in tests that
will be compiled locally into c-extension modules and loaded into python.
- numpy.testing.extbuild.build_and_import_extension(modname, functions, *, prologue='', build_dir=None, include_dirs=None, more_init='')#
Build and imports a c-extension module modname from a list of function fragments functions.
- 参数:
- functionslist of fragments
Each fragment is a sequence of func_name, calling convention, snippet.
- prologuestring
Code to precede the rest, usually extra
#includeor#definemacros.- build_dirpathlib.Path
Where to build the module, usually a temporary directory
- include_dirslist
Extra directories to find include files when compiling
- more_initstring
Code to appear in the module PyMODINIT_FUNC
- 返回:
- out: module
The module will have been loaded and is ready for use
示例
>>> 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')
Labeling tests#
Unlabeled tests like the ones above are run in the default
numpy.test() run. If you want to label your test as slow - and
therefore reserved for a full numpy.test(label='full') run, you
can label it with pytest.mark.slow:
import pytest
@pytest.mark.slow
def test_big(self):
print('Big, slow test')
Similarly for methods:
class test_zzz:
@pytest.mark.slow
def test_simple(self):
assert_(zzz() == 'Hello from zzz')
Easier setup and teardown functions / methods#
测试通过名称查找模块级别或类方法级别的设置和拆卸函数;因此:
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')
函数和方法的设置和拆卸函数被称为“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)
文档测试#
文档测试是一种记录函数行为并同时测试该行为的便捷方法.交互式 Python 会话的输出可以包含在函数的文档字符串中,测试框架可以运行该示例并将实际输出与预期输出进行比较.
可以通过将 doctests 参数添加到 test() 调用来运行文档测试;例如,要运行 numpy.lib 的所有测试(包括文档测试):
>>> import numpy as np
>>> np.lib.test(doctests=True)
文档测试的运行方式就像它们位于一个已经执行了 import numpy as np 的新的 Python 实例中一样.作为 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 的配置部分中添加 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):
...
当然,可以通过分别使用没有参数的 skip 或 xfail 来无条件地跳过测试或将其标记为已知失败.
跳过和已知失败测试的总数显示在测试运行结束时. 跳过的测试在测试结果中标记为 '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
要测试的包的完整路径.