Руководство по тестированию
Введение
До выпуска версии 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, используйте runtests.py. Чтобы запустить полный набор тестов NumPy:
$ python runtests.py
Тестирование подмножества NumPy:
$python runtests.py -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 и связанные функции используются для проверки того, является ли определенное предположение верным. Если утверждение не выполняется, тест терпит неудачу. pytest внутренне переписывает assert оператор, чтобы дать информативный вывод, когда он терпит неудачу, поэтому его следует предпочитать устаревшему варианту numpy.testing.assert_. Хотя простые assert операторы игнорируются при выполнении Python в оптимизированном режиме с -O, это не проблема при выполнении тестов с помощью pytest.
Аналогично, функции pytest pytest.raises и pytest.warns следует предпочитать их устаревшим аналогам numpy.testing.assert_raises и numpy.testing.assert_warns, так как варианты pytest используются более широко и позволяют более явно нацеливаться на предупреждения и ошибки при использовании с match регулярным выражением.
Обратите внимание, что test_ функции или методы не должны иметь строку документации, так как это затрудняет идентификацию теста из вывода выполнения набора тестов с verbose=2 (или аналогичной настройкой подробности). Используйте простые комментарии (#), если необходимо.
Также, поскольку большая часть кода NumPy является устаревшей и изначально написана без модульных тестов, все еще есть несколько модулей, у которых еще нет тестов. Не стесняйтесь выбрать один из этих модулей и разработать для него тесты.
Маркировка тестов
Немаркированные тесты, такие как те, что выше, выполняются в стандартном 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-level setup"""
print('doing setup')
def teardown():
"""Module-level teardown"""
print('doing teardown')
class TestMe:
def setup():
"""Class-level setup"""
print('doing setup')
def teardown():
"""Class-level teardown"""
print('doing teardown')
Функции настройки и завершения для функций и методов известны как «фиксатуры», и их использование не рекомендуется.
Параметрические тесты
Очень удобная особенность тестирования — возможность простого тестирования по диапазону параметров — неприятная проблема для стандартных модульных тестов. Используйте декоратор pytest.mark.parametrize.
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!
Советы и хитрости
Создание многих похожих тестов
Если у вас есть набор тестов, которые необходимо запускать многократно с незначительными вариациями, может быть полезно создать базовый класс, содержащий все общие тесты, а затем создать подкласс для каждой вариации. Несколько примеров этого приема существуют в NumPy; ниже приведены выдержки из одного в numpy/linalg/tests/test_linalg.py:
class LinalgTestCase:
def test_single(self):
a = array([[1., 2.], [3., 4.]], dtype=single)
b = array([2., 1.], dtype=single)
self.do(a, b)
def test_double(self):
a = array([[1., 2.], [3., 4.]], dtype=double)
b = array([2., 1.], dtype=double)
self.do(a, b)
...
class TestSolve(LinalgTestCase):
def do(self, a, b):
x = linalg.solve(a, b)
assert_allclose(b, dot(a, x))
assert imply(isinstance(b, matrix), isinstance(x, matrix))
class TestInv(LinalgTestCase):
def do(self, a, b):
a_inv = linalg.inv(a)
assert_allclose(dot(a, a_inv), identity(asarray(a).shape[0]))
assert imply(isinstance(a, matrix), isinstance(a_inv, matrix))
В этом случае мы хотели протестировать решение проблемы линейной алгебры, используя матрицы нескольких типов данных, используя linalg.solve и linalg.inv. Общие тестовые случаи (для матриц одинарной точности, двойной точности и т. д.) собраны в LinalgTestCase.
Известные сбои и пропуск тестов
Иногда вам может потребоваться пропустить тест или пометить его как известный сбой, например, когда набор тестов пишется до кода, который он должен проверить, или если тест завершается неудачей только на определенной архитектуре.
Чтобы пропустить тест, просто используйте 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–2022 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.21/reference/testing.html