Руководства по тестированию
Введение
До версии 1.15 NumPy использовал фреймворк для тестирования nose, сейчас используется pytest. Старый фреймворк по-прежнему поддерживается для поддержки проектов, использующих старый фреймворк numpy, но все тесты для NumPy должны использовать pytest.
Наша цель — обеспечить тщательный набор юнит-тестов для каждого модуля и пакета в NumPy. Эти тесты должны проверять полную функциональность заданной процедуры, а также ее устойчивость к ошибочным или неожиданным входным аргументам. Хорошо спроектированные тесты с хорошим покрытием значительно упрощают рефакторинг. Всякий раз, когда обнаруживается новая ошибка в процедуре, необходимо написать новый тест для этого конкретного случая и добавить его в набор тестов, чтобы предотвратить возвращение ошибки незамеченной.
Примечание
SciPy использует фреймворк для тестирования из numpy.testing, поэтому все примеры NumPy, показанные ниже, также применимы к SciPy
Тестирование NumPy
NumPy можно тестировать несколькими способами, выбирайте любой, который вам удобен.
Запуск тестов изнутри Python
Вы можете протестировать установленный NumPy, используя numpy.test, например, Для запуска полного набора тестов 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
Подробную информацию о тестировании см. в разделе Тестирование сборки
Другие способы запуска тестов
Запускайте тесты с помощью вашего любимого IDE, например, vscode или pycharm
Написание собственных тестов
Если вы пишете код, который хотели бы включить в NumPy, пожалуйста, пишите тесты по мере разработки кода. Каждый модуль Python, расширенный модуль или подпакет в каталоге пакета NumPy должен иметь соответствующий файл 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 или специализированная функция проверки, чтобы проверить, справедливо ли определенное предположение. Если проверка не выполняется, тест не выполняется. Общие функции проверки включают:
-
numpy.testing.assert_equalдля проверки точного поэлементного равенства между массивом результата и эталоном, -
numpy.testing.assert_allcloseдля проверки приблизительного поэлементного равенства между массивом результата и эталоном (т. е. с заданными относительными и абсолютными допусками), и -
numpy.testing.assert_array_lessдля проверки (строгого) поэлементного порядка между массивом результата и эталоном.
По умолчанию эти функции проверки сравнивают только числовые значения в массивах. Рассмотрите возможность использования опции strict=True, чтобы проверить также тип и форму массива.
Когда вам нужны пользовательские проверки, используйте оператор Python assert. Обратите внимание, что pytest внутренне переписывает операторы assert, чтобы обеспечить информативный вывод при ошибке, поэтому его следует предпочесть варианту legacy numpy.testing.assert_. В то время как обычные операторы assert игнорируются при запуске Python в оптимизированном режиме с -O, это не проблема при запуске тестов с pytest.
Аналогично, функции pytest pytest.raises и pytest.warns следует предпочесть их legacy аналогам numpy.testing.assert_raises и numpy.testing.assert_warns, которые используются более широко. Эти версии также принимают параметр match, который всегда следует использовать для точного указания на предполагаемое предупреждение или ошибку.
Обратите внимание, что функции или методы test_ не должны иметь docstring, так как это затрудняет идентификацию теста по результату выполнения набора тестов с помощью verbose=2 (или аналогичным параметром подробности). Используйте обычные комментарии (#), чтобы описать намерение теста и помочь непосвященному читателю интерпретировать код.
Кроме того, поскольку значительная часть кода NumPy является legacy-кодом, который изначально был написан без юнит-тестов, до сих пор есть несколько модулей, для которых ещё нет тестов. Не стесняйтесь выбрать один из этих модулей и разработать для него тесты.
Использование кода C в тестах
NumPy предоставляет богатый C-API. Они тестируются с помощью модулей c-расширений, написанных «как если бы» они ничего не знали о внутренней работе NumPy, а только используя официальные интерфейсы 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='')
-
Сборка и импорт модуля c-расширения
modnameиз списка фрагментов функцийfunctions.- Параметры:
-
- functionsсписок фрагментов
-
Каждый фрагмент — это последовательность func_name, соглашения о вызове, фрагмент.
- прологстрока
-
Код, который предшествует остальным, обычно дополнительные
#includeили#defineмакросы. - build_dirпуть pathlib.Path
-
Место для сборки модуля, обычно временная директория
- include_dirsсписок
-
Дополнительные каталоги для поиска файлов include при компиляции
- 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')
Функции подготовки и завершения, относящиеся к функциям и методам, известны как «фиксатуры», и их следует использовать экономно. 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 можно включить в строку документации функции, и фреймворк тестирования может выполнить пример и сравнить фактический вывод с ожидаемым.
Doctests можно запустить, добавив аргумент doctests к вызову test(); например, чтобы запустить все тесты (включая doctests) для numpy.lib:
>>> 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/. Для нашего примера, если она еще не существует, вам нужно создать директорию 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__.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).
Тесты на случайных данных
Тесты на случайных данных полезны, но поскольку ошибки тестов предназначены для выявления новых ошибок или регрессий, тест, который проходит большую часть времени, но периодически терпит неудачу без внесения изменений в код, не является полезным. Сделайте случайные данные детерминированными, установив начальное значение генератора случайных чисел перед его генерацией. Используйте либо функцию random.seed(some_number) Python, либо функцию numpy.random.seed(some_number) NumPy, в зависимости от источника случайных чисел.
В качестве альтернативы, можно использовать 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
-
Полный путь к пакету для тестирования.
© 2005–2024 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/2.0/reference/testing.html