Рекомендации по тестированию
Введение
До релиза 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')
Чаще всего нам нужно сгруппировать несколько тестов вместе, поэтому мы создаем класс тестов:
from numpy.testing import assert_, assert_raises
# import xxx symbols
from numpy.xxx.yyy import zzz
class TestZzz:
def test_simple(self):
assert_(zzz() == 'Hello from zzz')
def test_invalid_parameter(self):
assert_raises(...)
В этих методах тестов assert_() и связанные функции используются для проверки того, является ли определенное предположение допустимым. Если утверждение не выполняется, тест завершается неудачей. Обратите внимание, что встроенная функция Python assert не должна использоваться, так как она удаляется во время компиляции с -O.
Обратите внимание, что 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'
Функции подготовки и завершения к функциям и методам известны как «фиксатуры», и их использование не рекомендуется.
Параметрические тесты
Очень удобная функция тестирования — это возможность лёгкого тестирования набора параметров — сложная проблема для стандартных модульных тестов. Используйте декоратор dec.paramaterize.
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_almost_equal(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_almost_equal(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).
Тесты на случайных данных
Тесты на случайных данных полезны, но поскольку сбои тестов предназначены для выявления новых ошибок или регрессий, тест, который проходит большую часть времени, но периодически терпит неудачу без изменений кода, не является полезным. Сделайте случайные данные детерминированными, установив начальное значение генератора случайных чисел перед его генерацией. Используйте либо встроенный генератор случайных чисел 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_namemodule name -
Имя модуля для тестирования.
-
Примечания
В отличие от предыдущей реализации на основе
nose, этот класс не публикуется, так как он выполняет подавление некоторыхnumpy-специфичных предупреждений.- Атрибуты
-
-
module_namestr -
Полный путь к пакету для тестирования.
-
© 2005–2021 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.20/reference/testing.html