Рекомендации по тестированию#

Введение#

До выпуска 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 'fmin'>

>>> 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

Подробную информацию о тестировании см. в Тестирование сборок

Запуск тестов в нескольких потоках#

Для помощи в стресс-тестировании NumPy на безопасность потоков, набор тестов может быть запущен под pytest-run-parallel. Для установки pytest-run-parallel:

$ pip install pytest-run-parallel

Для запуска набора тестов в нескольких потоках:

$ spin test -p auto # have pytest-run-parallel detect the number of available cores
$ spin test -p 4 # run each test under 4 threads
$ spin test -p auto -- --skip-thread-unsafe=true # run ONLY tests that are thread-safe

При написании новых тестов стоит проверять, чтобы они не проваливались при pytest-run-parallel, поскольку задания CI используют его. Некоторые советы о том, как писать потокобезопасные тесты, можно найти здесь.

Примечание

В идеале следует запустить pytest-run-parallel используя свободнопоточная сборка Python который равен 3.14 или выше. Если вы решите использовать версию Python без свободной потоковой модели, вам нужно будет установить переменные окружения PYTHON_CONTEXT_AWARE_WARNINGS и PYTHON_THREAD_INHERIT_CONTEXT до 1.

Запуск доктестов#

Документация 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'

Обратите внимание, что doctests не запускаются при использовании spin test.

Другие методы запуска тестов#

Запускайте тесты с помощью вашей любимой IDE, например vscode или pycharm

Написание собственных тестов#

Если вы пишете код, который вы хотели бы включить в NumPy, пожалуйста, пишите тесты по мере разработки вашего кода. Каждый модуль Python, модуль расширения или подпакет в каталоге пакета NumPy должен иметь соответствующий test_.py файл. Pytest проверяет эти файлы на наличие тестовых методов (названных test*) и тестовые классы (названные Test*).

Предположим, у вас есть модуль NumPy numpy/xxx/yyy.py содержащий функцию zzz(). Чтобы протестировать эту функцию, вы создадите тестовый модуль с именем test_yyy.py. Если вам нужно проверить только один аспект zzz, вы можете просто добавить тестовую функцию:

def test_zzz():
    assert zzz() == 'Hello from zzz'

Чаще нам нужно сгруппировать несколько тестов вместе, поэтому мы создаем класс теста:

# 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 опцию для проверки типа данных и формы массива.

. Для простых типов с assert утверждение. Обратите внимание, что pytest внутренне переписывает assert утверждения для предоставления информативного вывода при неудаче, поэтому его следует предпочесть устаревшему варианту numpy.testing.assert_. В то время как обычный assert операторы игнорируются при запуске Python в оптимизированном режиме с -O, это не проблема при запуске тестов с pytest.

Аналогично, функции pytest pytest.raises и pytest.warns следует предпочесть их устаревшим аналогам numpy.testing.assert_raises и numpy.testing.assert_warns, которые используются более широко. Эти версии также принимают match параметр, который всегда следует использовать для точного нацеливания на предполагаемое предупреждение или ошибку.

Обратите внимание, что test_ функции или методы не должны иметь документации, потому что это затрудняет идентификацию теста из вывода запуска набора тестов с verbose=2 (или аналогичная настройка подробности). Используйте обычные комментарии (#) для описания цели теста и помощи незнакомому читателю в интерпретации кода.

Кроме того, поскольку большая часть NumPy — это устаревший код, изначально написанный без модульных тестов, всё ещё есть несколько модулей, для которых тесты отсутствуют. Пожалуйста, не стесняйтесь выбрать один из этих модулей и разработать для него тесты.

Использование C-кода в тестах#

NumPy предоставляет богатый C-API . Они тестируются с использованием модулей C-расширений, написанных «как будто» они ничего не знают о внутреннем устройстве NumPy, а используют только официальные C-API интерфейсы. Примерами таких модулей являются тесты для пользовательского rational dtype в _rational_tests или тесты механизма ufunc в _umath_tests которые являются частью бинарного дистрибутива. Начиная с версии 1.21, вы также можете писать фрагменты кода на C в тестах, которые будут скомпилированы локально в c-расширения и загружены в Python.

numpy.testing.extbuild.build_and_import_extension(modname, функции, *, пролог='', build_dir=None, include_dirs=None, more_init='')#

Собирает и импортирует c-расширение модуля modname из списка фрагментов функций функции.

Параметры:
функциисписок фрагментов

Каждый фрагмент представляет собой последовательность func_name, соглашение о вызове, сниппет.

прологstring

Код, предшествующий остальному, обычно дополнительный #include или #define макросы.

build_dirpathlib.Path

Где собирать модуль, обычно временный каталог

include_dirslist

Дополнительные каталоги для поиска включаемых файлов при компиляции

more_initstring

Код, который появится в модуле PyMODINIT_FUNC

Возвращает:
выход: модуль

Модуль будет загружен и готов к использованию

Примеры

>>> 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')

Методы настройки и завершения#

NumPy изначально использовал xunit setup и teardown, функцию pytest. Теперь мы рекомендуем использование методов setup и teardown, которые явно вызываются тестами, которые в них нуждаются:

class TestMe:
    def setup(self):
        print('doing setup')
        return 1

    def teardown(self):
        print('doing teardown')

    def test_xyz(self):
        x = self.setup()
        assert x == 1
        self.teardown()

Этот подход потокобезопасен, гарантируя, что тесты могут выполняться под pytest-run-parallel. Использование фикстур настройки pytest (таких как методы настройки xunit) обычно не является потокобезопасным и, вероятно, вызовет сбои тестов на потокобезопасность.

pytest поддерживает более общие фикстуры на различных уровнях, которые могут использоваться автоматически через специальные аргументы. Например, специальное имя аргумента tmp_path используется в тестах для создания временных каталогов. Однако фикстуры следует использовать экономно.

Параметрические тесты#

Одна очень удобная функция 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 аргумент для test() вызов; например, чтобы запустить все тесты (включая doctests) для numpy.lib:

>>> import numpy as np
>>> np.lib.test(doctests=True)

Доктесты выполняются так, как если бы они находились в новом экземпляре 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' if verbose > 1).

Тесты на случайных данных#

Тесты на случайных данных хороши, но поскольку сбои тестов предназначены для выявления новых ошибок или регрессий, тест, который проходит большую часть времени, но иногда даёт сбой без изменений кода, не полезен. Сделайте случайные данные детерминированными, установив начальное значение генератора случайных чисел перед их созданием. Используйте rng = numpy.random.RandomState(some_number) для установки seed на локальном экземпляре numpy.random.RandomState.

В качестве альтернативы вы можете использовать Гипотеза для генерации произвольных данных. Hypothesis управляет случайными семенами Python и Numpy за вас и предоставляет очень краткий и мощный способ описания данных (включая hypothesis.extra.numpy, например, для набора взаимно broadcastable форм).

Преимущества перед случайной генерацией включают инструменты для воспроизведения и обмена сбоями без необходимости фиксированного сида, отчётности минимальный примеров для каждого сбоя и методы лучше, чем наивный случайный выбор, для выявления ошибок.

Написание потокобезопасных тестов#

Написание потокобезопасных тестов может потребовать некоторых проб и ошибок. Обычно следует придерживаться указанных выше рекомендаций, особенно когда речь идёт о методы настройки и инициализация случайных данных. Явная настройка и использование локального ГСЧ являются потокобезопасными практиками. Вот советы по некоторым другим распространённым проблемам, с которыми вы можете столкнуться.

Используя pytest.mark.parametrize может иногда вызывать проблемы с потокобезопасностью. Чтобы исправить это, можно использовать copy():

@pytest.mark.parametrize('dimensionality', [3, 10, 25])
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
def test_solve(dimensionality, dtype):
    dimen = dimensionality.copy()
    d = dtype.copy()
    # use these copied variables instead
    ...

Если вы тестируете что-то, что по своей природе небезопасно для потоков, вы можете пометить свой тест pytest.mark.thread_unsafe чтобы он выполнялся в одном потоке и не вызывал сбоев тестов:

@pytest.mark.thread_unsafe(reason="reason this test is thread-unsafe")
def test_thread_unsafe():
  ...

Некоторые примеры того, что следует помечать как небезопасное для потоков:

  • Использование sys.stdout и sys.stderr

  • Изменение глобальных данных, таких как строки документации, модули, сборщики мусора и т.д.

  • Тесты, требующие много памяти, поскольку они могут вызывать сбои.

Кроме того, некоторые pytest фикстуры небезопасны для потоков, такие как monkeypatch и capsys. Однако, pytest-run-parallel автоматически пометит их как небезопасные для потоков, если вы решите их использовать. Некоторые фикстуры были исправлены, чтобы быть потокобезопасными, например, tmp_path.

Документация для numpy.test#

numpy.тест(метка='fast', verbose=1, extra_argv=None, doctests=False, покрытие=False, длительности=-1, тесты=None)#

Тестовый фреймворк Pytest.

Тестовая функция обычно добавляется в __init__.py пакета следующим образом:

from numpy._pytesttester import PytestTester
test = PytestTester(__name__).test
del PytestTester

Вызов этой тестовой функции находит и запускает все тесты, связанные с модулем и всеми его подмодулями.

Параметры:
module_nameимя модуля

Имя модуля для тестирования.

Примечания

В отличие от предыдущего nose-основанная реализация, этот класс не публично доступен, так как выполняет некоторые numpy-специфичное подавление предупреждений.

Атрибуты:
module_namestr

Полный путь к пакету для тестирования.